From 682836237bcae3928baa6332ac5b6a27dbc1ecca Mon Sep 17 00:00:00 2001 From: kikootwo Date: Mon, 12 Jan 2026 12:45:48 -0500 Subject: [PATCH] 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 --- documentation/TABLEOFCONTENTS.md | 6 + documentation/backend/services/logging.md | 130 +++++++++++ documentation/phase3/qbittorrent.md | 5 + src/app/api/admin/backend-mode/route.ts | 9 +- src/app/api/admin/bookdate/toggle/route.ts | 5 +- src/app/api/admin/downloads/active/route.ts | 7 +- src/app/api/admin/job-status/[id]/route.ts | 11 +- src/app/api/admin/jobs/[id]/route.ts | 7 +- src/app/api/admin/jobs/[id]/trigger/route.ts | 9 +- src/app/api/admin/jobs/route.ts | 7 +- src/app/api/admin/logs/route.ts | 5 +- src/app/api/admin/metrics/route.ts | 5 +- src/app/api/admin/plex/scan/route.ts | 5 +- src/app/api/admin/requests/[id]/route.ts | 5 +- src/app/api/admin/requests/recent/route.ts | 5 +- .../audiobookshelf/libraries/route.ts | 13 +- .../admin/settings/audiobookshelf/route.ts | 5 +- .../admin/settings/change-password/route.ts | 7 +- .../admin/settings/download-client/route.ts | 7 +- src/app/api/admin/settings/ebook/route.ts | 5 +- .../settings/ebook/test-flaresolverr/route.ts | 5 +- src/app/api/admin/settings/oidc/route.ts | 5 +- src/app/api/admin/settings/paths/route.ts | 7 +- .../admin/settings/plex/libraries/route.ts | 5 +- src/app/api/admin/settings/plex/route.ts | 13 +- .../admin/settings/prowlarr/indexers/route.ts | 7 +- src/app/api/admin/settings/prowlarr/route.ts | 7 +- .../api/admin/settings/registration/route.ts | 5 +- src/app/api/admin/settings/route.ts | 5 +- .../settings/test-download-client/route.ts | 11 +- src/app/api/admin/settings/test-plex/route.ts | 5 +- .../api/admin/settings/test-prowlarr/route.ts | 5 +- src/app/api/admin/users/[id]/approve/route.ts | 5 +- src/app/api/admin/users/[id]/route.ts | 7 +- src/app/api/admin/users/pending/route.ts | 5 +- src/app/api/admin/users/route.ts | 5 +- src/app/api/audiobooks/[asin]/route.ts | 5 +- src/app/api/audiobooks/covers/route.ts | 5 +- src/app/api/audiobooks/new-releases/route.ts | 5 +- src/app/api/audiobooks/popular/route.ts | 5 +- .../audiobooks/request-with-torrent/route.ts | 9 +- .../api/audiobooks/search-torrents/route.ts | 56 ++--- src/app/api/audiobooks/search/route.ts | 5 +- src/app/api/auth/admin/login/route.ts | 7 +- src/app/api/auth/local/login/route.ts | 17 +- src/app/api/auth/oidc/callback/route.ts | 7 +- src/app/api/auth/oidc/login/route.ts | 5 +- src/app/api/auth/plex/callback/route.ts | 35 +-- src/app/api/auth/plex/home-users/route.ts | 5 +- src/app/api/auth/plex/login/route.ts | 5 +- src/app/api/auth/plex/switch-profile/route.ts | 13 +- src/app/api/auth/providers/route.ts | 5 +- src/app/api/auth/refresh/route.ts | 5 +- src/app/api/auth/register/route.ts | 5 +- src/app/api/bookdate/config/route.ts | 9 +- src/app/api/bookdate/generate/route.ts | 17 +- src/app/api/bookdate/preferences/route.ts | 7 +- src/app/api/bookdate/recommendations/route.ts | 25 +- src/app/api/bookdate/swipe/route.ts | 11 +- src/app/api/bookdate/swipes/route.ts | 7 +- src/app/api/bookdate/test-connection/route.ts | 15 +- src/app/api/bookdate/undo/route.ts | 5 +- .../api/cache/thumbnails/[filename]/route.ts | 5 +- src/app/api/config/[category]/route.ts | 5 +- src/app/api/config/route.ts | 7 +- src/app/api/health/route.ts | 5 +- src/app/api/init/route.ts | 9 +- .../api/requests/[id]/fetch-ebook/route.ts | 27 ++- .../requests/[id]/interactive-search/route.ts | 59 ++--- .../api/requests/[id]/manual-search/route.ts | 5 +- src/app/api/requests/[id]/route.ts | 9 +- .../api/requests/[id]/select-torrent/route.ts | 7 +- src/app/api/requests/route.ts | 9 +- src/app/api/setup/complete/route.ts | 21 +- src/app/api/setup/status/route.ts | 5 +- .../api/setup/test-download-client/route.ts | 5 +- src/app/api/setup/test-oidc/route.ts | 5 +- src/app/api/setup/test-paths/route.ts | 15 +- src/app/api/setup/test-plex/route.ts | 5 +- src/app/api/setup/test-prowlarr/route.ts | 5 +- src/lib/bookdate/helpers.ts | 94 ++++---- src/lib/integrations/audible.service.ts | 90 ++++---- src/lib/integrations/plex.service.ts | 182 +++++++-------- src/lib/integrations/prowlarr.service.ts | 69 +++--- src/lib/integrations/qbittorrent.service.ts | 132 ++++++----- src/lib/integrations/sabnzbd.service.ts | 17 +- src/lib/middleware/auth.ts | 9 +- .../processors/audible-refresh.processor.ts | 22 +- .../cleanup-seeded-torrents.processor.ts | 32 +-- .../processors/download-torrent.processor.ts | 26 +-- src/lib/processors/match-plex.processor.ts | 22 +- .../processors/monitor-download.processor.ts | 24 +- .../processors/monitor-rss-feeds.processor.ts | 24 +- .../processors/organize-files.processor.ts | 27 ++- .../plex-recently-added.processor.ts | 28 +-- .../retry-failed-imports.processor.ts | 46 ++-- .../retry-missing-torrents.processor.ts | 16 +- src/lib/processors/scan-plex.processor.ts | 56 ++--- .../processors/search-indexers.processor.ts | 64 +++--- src/lib/services/audiobookshelf/api.ts | 5 +- src/lib/services/auth/LocalAuthProvider.ts | 17 +- src/lib/services/auth/OIDCAuthProvider.ts | 43 ++-- src/lib/services/auth/PlexAuthProvider.ts | 9 +- src/lib/services/config.service.ts | 11 +- src/lib/services/ebook-scraper.ts | 133 ++++------- src/lib/services/job-queue.service.ts | 27 ++- .../services/library/PlexLibraryService.ts | 5 +- src/lib/services/request-delete.service.ts | 79 +++---- src/lib/services/scheduler.service.ts | 37 +-- src/lib/services/thumbnail-cache.service.ts | 21 +- src/lib/utils/audiobook-matcher.ts | 106 ++++----- src/lib/utils/file-organizer.ts | 21 +- src/lib/utils/job-logger.ts | 83 +++---- src/lib/utils/jwt-client.ts | 6 +- src/lib/utils/jwt.ts | 10 +- src/lib/utils/logger.ts | 213 ++++++++++++++++++ src/lib/utils/path-mapper.ts | 13 +- src/lib/utils/url.ts | 20 +- 118 files changed, 1623 insertions(+), 1079 deletions(-) create mode 100644 documentation/backend/services/logging.md create mode 100644 src/lib/utils/logger.ts diff --git a/documentation/TABLEOFCONTENTS.md b/documentation/TABLEOFCONTENTS.md index 0d87a7c..169efa9 100644 --- a/documentation/TABLEOFCONTENTS.md +++ b/documentation/TABLEOFCONTENTS.md @@ -55,6 +55,11 @@ - **Scheduled/recurring jobs (cron)** → [backend/services/scheduler.md](backend/services/scheduler.md) - **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 - **Component catalog (cards, badges, forms)** → [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) **"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 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 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) diff --git a/documentation/backend/services/logging.md b/documentation/backend/services/logging.md new file mode 100644 index 0000000..ff180a5 --- /dev/null +++ b/documentation/backend/services/logging.md @@ -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 diff --git a/documentation/phase3/qbittorrent.md b/documentation/phase3/qbittorrent.md index 8ca0bc8..96d96a5 100644 --- a/documentation/phase3/qbittorrent.md +++ b/documentation/phase3/qbittorrent.md @@ -176,6 +176,11 @@ type TorrentState = 'downloading' | 'uploading' | 'stalledDL' | - Enhanced error messages identifying SSL/TLS certificate issues with actionable guidance - Secure by default (SSL verification enabled), with clear security warnings when disabled - 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 diff --git a/src/app/api/admin/backend-mode/route.ts b/src/app/api/admin/backend-mode/route.ts index 9065ab8..5040341 100644 --- a/src/app/api/admin/backend-mode/route.ts +++ b/src/app/api/admin/backend-mode/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -18,7 +21,7 @@ export async function GET(request: NextRequest) { isAudiobookshelf: backendMode === 'audiobookshelf' }); } 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( { error: 'Failed to get backend mode' }, { status: 500 } @@ -50,7 +53,7 @@ export async function PUT(request: NextRequest) { const { clearLibraryServiceCache } = await import('@/lib/services/library'); clearLibraryServiceCache(); - console.log(`[BackendMode] Backend mode changed to: ${mode}`); + logger.info(`Backend mode changed to: ${mode}`); return NextResponse.json({ success: true, @@ -58,7 +61,7 @@ export async function PUT(request: NextRequest) { message: `Backend mode set to ${mode}` }); } 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( { error: 'Failed to set backend mode' }, { status: 500 } diff --git a/src/app/api/admin/bookdate/toggle/route.ts b/src/app/api/admin/bookdate/toggle/route.ts index 1c85580..bafe660 100644 --- a/src/app/api/admin/bookdate/toggle/route.ts +++ b/src/app/api/admin/bookdate/toggle/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Admin.BookDate.Toggle'); async function handler(req: AuthenticatedRequest) { try { @@ -31,7 +34,7 @@ async function handler(req: AuthenticatedRequest) { }); } 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( { error: error.message || 'Failed to toggle BookDate' }, { status: 500 } diff --git a/src/app/api/admin/downloads/active/route.ts b/src/app/api/admin/downloads/active/route.ts index 21b4522..c19ecee 100644 --- a/src/app/api/admin/downloads/active/route.ts +++ b/src/app/api/admin/downloads/active/route.ts @@ -9,6 +9,9 @@ import { prisma } from '@/lib/db'; import { getQBittorrentService } from '@/lib/integrations/qbittorrent.service'; import { getSABnzbdService } from '@/lib/integrations/sabnzbd.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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -96,7 +99,7 @@ export async function GET(request: NextRequest) { } } catch (error) { // 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 { @@ -117,7 +120,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ downloads: formatted }); } 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( { error: 'Failed to fetch active downloads' }, { status: 500 } diff --git a/src/app/api/admin/job-status/[id]/route.ts b/src/app/api/admin/job-status/[id]/route.ts index 5cb29e2..fee797f 100644 --- a/src/app/api/admin/job-status/[id]/route.ts +++ b/src/app/api/admin/job-status/[id]/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { verifyAccessToken } from '@/lib/utils/jwt'; 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 @@ -30,17 +33,17 @@ export async function GET( // Await params in Next.js 15+ 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 job = await jobQueueService.getJob(id); if (!job) { - console.log(`[JobStatus] Job not found: ${id}`); + logger.debug(`Job not found: ${id}`); 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({ success: true, @@ -58,7 +61,7 @@ export async function GET( }, }); } 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( { error: 'InternalError', diff --git a/src/app/api/admin/jobs/[id]/route.ts b/src/app/api/admin/jobs/[id]/route.ts index 15d9ac3..aa26d94 100644 --- a/src/app/api/admin/jobs/[id]/route.ts +++ b/src/app/api/admin/jobs/[id]/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { verifyAccessToken } from '@/lib/utils/jwt'; 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 @@ -45,7 +48,7 @@ export async function PUT( job, }); } 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( { error: 'InternalError', @@ -87,7 +90,7 @@ export async function DELETE( message: 'Job deleted successfully', }); } 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( { error: 'InternalError', diff --git a/src/app/api/admin/jobs/[id]/trigger/route.ts b/src/app/api/admin/jobs/[id]/trigger/route.ts index e9ad09d..b6a6f8c 100644 --- a/src/app/api/admin/jobs/[id]/trigger/route.ts +++ b/src/app/api/admin/jobs/[id]/trigger/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { verifyAccessToken } from '@/lib/utils/jwt'; 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 @@ -30,12 +33,12 @@ export async function POST( // Await params in Next.js 15+ const { id } = await params; - console.log(`[JobTrigger] Triggering scheduled job: ${id}`); + logger.info(`Triggering scheduled job: ${id}`); const schedulerService = getSchedulerService(); 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({ success: true, @@ -43,7 +46,7 @@ export async function POST( message: 'Job triggered successfully', }); } 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( { error: 'InternalError', diff --git a/src/app/api/admin/jobs/route.ts b/src/app/api/admin/jobs/route.ts index b755925..d7c18e4 100644 --- a/src/app/api/admin/jobs/route.ts +++ b/src/app/api/admin/jobs/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { verifyAccessToken } from '@/lib/utils/jwt'; import { getSchedulerService } from '@/lib/services/scheduler.service'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Admin.Jobs'); /** * GET /api/admin/jobs @@ -31,7 +34,7 @@ export async function GET(request: NextRequest) { jobs, }); } 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( { error: 'InternalError', @@ -74,7 +77,7 @@ export async function POST(request: NextRequest) { job, }); } 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( { error: 'InternalError', diff --git a/src/app/api/admin/logs/route.ts b/src/app/api/admin/logs/route.ts index 2640d03..0578219 100644 --- a/src/app/api/admin/logs/route.ts +++ b/src/app/api/admin/logs/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Admin.Logs'); export async function GET(request: NextRequest) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -94,7 +97,7 @@ export async function GET(request: NextRequest) { }, }); } 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( { error: 'Failed to fetch logs' }, { status: 500 } diff --git a/src/app/api/admin/metrics/route.ts b/src/app/api/admin/metrics/route.ts index 04b20e6..1095d24 100644 --- a/src/app/api/admin/metrics/route.ts +++ b/src/app/api/admin/metrics/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Admin.Metrics'); export async function GET(request: NextRequest) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -77,7 +80,7 @@ export async function GET(request: NextRequest) { systemHealth, }); } 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( { error: 'Failed to fetch metrics' }, { status: 500 } diff --git a/src/app/api/admin/plex/scan/route.ts b/src/app/api/admin/plex/scan/route.ts index 7b6a68e..06039b0 100644 --- a/src/app/api/admin/plex/scan/route.ts +++ b/src/app/api/admin/plex/scan/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin } from '@/lib/middleware/auth'; 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 @@ -27,7 +30,7 @@ export async function POST(request: NextRequest) { ...result, }); } 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( { error: 'ScanFailed', diff --git a/src/app/api/admin/requests/[id]/route.ts b/src/app/api/admin/requests/[id]/route.ts index f46086f..e8ec01e 100644 --- a/src/app/api/admin/requests/[id]/route.ts +++ b/src/app/api/admin/requests/[id]/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; 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] @@ -62,7 +65,7 @@ export async function DELETE( }, }); } 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( { error: 'DeleteError', diff --git a/src/app/api/admin/requests/recent/route.ts b/src/app/api/admin/requests/recent/route.ts index 4182d99..fe99aa4 100644 --- a/src/app/api/admin/requests/recent/route.ts +++ b/src/app/api/admin/requests/recent/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -61,7 +64,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ requests: formatted }); } 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( { error: 'Failed to fetch recent requests' }, { status: 500 } diff --git a/src/app/api/admin/settings/audiobookshelf/libraries/route.ts b/src/app/api/admin/settings/audiobookshelf/libraries/route.ts index 0075b74..9fe0b6e 100644 --- a/src/app/api/admin/settings/audiobookshelf/libraries/route.ts +++ b/src/app/api/admin/settings/audiobookshelf/libraries/route.ts @@ -5,20 +5,23 @@ import { NextRequest, NextResponse } from 'next/server'; 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) { - console.log('[ABS Libraries] GET request received'); + logger.debug('GET request received'); 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 () => { - console.log('[ABS Libraries] Admin check passed'); + logger.debug('Admin check passed'); try { // Use getConfigService like Plex endpoint does const { getConfigService } = await import('@/lib/services/config.service'); const configService = getConfigService(); const serverUrl = await configService.get('audiobookshelf.server_url'); 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) { return NextResponse.json( @@ -55,7 +58,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ libraries }); } 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( { error: 'Failed to fetch libraries' }, { status: 500 } diff --git a/src/app/api/admin/settings/audiobookshelf/route.ts b/src/app/api/admin/settings/audiobookshelf/route.ts index bf9741e..8c64e83 100644 --- a/src/app/api/admin/settings/audiobookshelf/route.ts +++ b/src/app/api/admin/settings/audiobookshelf/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -41,7 +44,7 @@ export async function PUT(request: NextRequest) { message: 'Audiobookshelf settings saved successfully' }); } 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( { error: 'Failed to save settings' }, { status: 500 } diff --git a/src/app/api/admin/settings/change-password/route.ts b/src/app/api/admin/settings/change-password/route.ts index d58722e..aa5618b 100644 --- a/src/app/api/admin/settings/change-password/route.ts +++ b/src/app/api/admin/settings/change-password/route.ts @@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireLocalAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; import bcrypt from 'bcrypt'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Admin.Settings.ChangePassword'); /** * 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({ success: true, message: 'Password changed successfully', }); } 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( { success: false, diff --git a/src/app/api/admin/settings/download-client/route.ts b/src/app/api/admin/settings/download-client/route.ts index 2c65bf3..673b887 100644 --- a/src/app/api/admin/settings/download-client/route.ts +++ b/src/app/api/admin/settings/download-client/route.ts @@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -135,7 +138,7 @@ export async function PUT(request: NextRequest) { 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 if (type === 'qbittorrent') { @@ -151,7 +154,7 @@ export async function PUT(request: NextRequest) { message: 'Download client settings updated successfully', }); } 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( { success: false, diff --git a/src/app/api/admin/settings/ebook/route.ts b/src/app/api/admin/settings/ebook/route.ts index 5fd9934..0a9115f 100644 --- a/src/app/api/admin/settings/ebook/route.ts +++ b/src/app/api/admin/settings/ebook/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -73,7 +76,7 @@ export async function PUT(request: NextRequest) { return NextResponse.json({ success: true }); } 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( { error: 'Failed to save settings' }, { status: 500 } diff --git a/src/app/api/admin/settings/ebook/test-flaresolverr/route.ts b/src/app/api/admin/settings/ebook/test-flaresolverr/route.ts index 8fdf721..f9b7f67 100644 --- a/src/app/api/admin/settings/ebook/test-flaresolverr/route.ts +++ b/src/app/api/admin/settings/ebook/test-flaresolverr/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -31,7 +34,7 @@ export async function POST(request: NextRequest) { return NextResponse.json(result); } catch (error) { - console.error('FlareSolverr test failed:', error); + logger.error('FlareSolverr test failed', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json( { success: false, diff --git a/src/app/api/admin/settings/oidc/route.ts b/src/app/api/admin/settings/oidc/route.ts index 68ba7cf..7e044c4 100644 --- a/src/app/api/admin/settings/oidc/route.ts +++ b/src/app/api/admin/settings/oidc/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -62,7 +65,7 @@ export async function PUT(request: NextRequest) { message: 'OIDC settings saved successfully' }); } 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( { error: 'Failed to save settings' }, { status: 500 } diff --git a/src/app/api/admin/settings/paths/route.ts b/src/app/api/admin/settings/paths/route.ts index d5e1771..a24e6e3 100644 --- a/src/app/api/admin/settings/paths/route.ts +++ b/src/app/api/admin/settings/paths/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; 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) { 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 const { invalidateQBittorrentService } = await import('@/lib/integrations/qbittorrent.service'); @@ -76,7 +79,7 @@ export async function PUT(request: NextRequest) { message: 'Paths settings updated successfully', }); } 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( { success: false, diff --git a/src/app/api/admin/settings/plex/libraries/route.ts b/src/app/api/admin/settings/plex/libraries/route.ts index 8a9bd4a..48a15d0 100644 --- a/src/app/api/admin/settings/plex/libraries/route.ts +++ b/src/app/api/admin/settings/plex/libraries/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; 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 @@ -51,7 +54,7 @@ export async function GET(request: NextRequest) { })), }); } 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( { success: false, diff --git a/src/app/api/admin/settings/plex/route.ts b/src/app/api/admin/settings/plex/route.ts index f93ac78..8f58b6b 100644 --- a/src/app/api/admin/settings/plex/route.ts +++ b/src/app/api/admin/settings/plex/route.ts @@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -69,24 +72,24 @@ export async function PUT(request: NextRequest) { update: { 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 { - console.warn('[Admin] Could not fetch machineIdentifier'); + logger.warn('Could not fetch machineIdentifier'); } } } 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 } - console.log('[Admin] Plex settings updated'); + logger.info('Plex settings updated'); return NextResponse.json({ success: true, message: 'Plex settings updated successfully', }); } 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( { success: false, diff --git a/src/app/api/admin/settings/prowlarr/indexers/route.ts b/src/app/api/admin/settings/prowlarr/indexers/route.ts index a085aca..050e667 100644 --- a/src/app/api/admin/settings/prowlarr/indexers/route.ts +++ b/src/app/api/admin/settings/prowlarr/indexers/route.ts @@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { getProwlarrService } from '@/lib/integrations/prowlarr.service'; import { getConfigService } from '@/lib/services/config.service'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Admin.Settings.ProwlarrIndexers'); interface SavedIndexerConfig { id: number; @@ -65,7 +68,7 @@ export async function GET(request: NextRequest) { flagConfigs, }); } 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( { success: false, @@ -128,7 +131,7 @@ export async function PUT(request: NextRequest) { message: 'Indexer configuration saved', }); } 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( { success: false, diff --git a/src/app/api/admin/settings/prowlarr/route.ts b/src/app/api/admin/settings/prowlarr/route.ts index 63bd591..7d54595 100644 --- a/src/app/api/admin/settings/prowlarr/route.ts +++ b/src/app/api/admin/settings/prowlarr/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; 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) { 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({ success: true, message: 'Prowlarr settings updated successfully', }); } 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( { success: false, diff --git a/src/app/api/admin/settings/registration/route.ts b/src/app/api/admin/settings/registration/route.ts index 551f1ec..102f773 100644 --- a/src/app/api/admin/settings/registration/route.ts +++ b/src/app/api/admin/settings/registration/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -26,7 +29,7 @@ export async function PUT(request: NextRequest) { message: 'Registration settings saved successfully' }); } 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( { error: 'Failed to save settings' }, { status: 500 } diff --git a/src/app/api/admin/settings/route.ts b/src/app/api/admin/settings/route.ts index 26b6bda..438477b 100644 --- a/src/app/api/admin/settings/route.ts +++ b/src/app/api/admin/settings/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Admin.Settings'); export async function GET(request: NextRequest) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -103,7 +106,7 @@ export async function GET(request: NextRequest) { return NextResponse.json(settings); } 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( { error: 'Failed to fetch settings' }, { status: 500 } diff --git a/src/app/api/admin/settings/test-download-client/route.ts b/src/app/api/admin/settings/test-download-client/route.ts index b8eb3a8..fae9c88 100644 --- a/src/app/api/admin/settings/test-download-client/route.ts +++ b/src/app/api/admin/settings/test-download-client/route.ts @@ -8,6 +8,9 @@ import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middlewar import { prisma } from '@/lib/db'; import { QBittorrentService } from '@/lib/integrations/qbittorrent.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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -24,7 +27,7 @@ export async function POST(request: NextRequest) { localPath, } = 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) { return NextResponse.json( @@ -61,7 +64,7 @@ export async function POST(request: NextRequest) { let version: string | undefined; if (type === 'qbittorrent') { - console.log('[TestDownloadClient] Testing qBittorrent connection'); + logger.debug('Testing qBittorrent connection'); if (!username || !actualPassword) { return NextResponse.json( { success: false, error: 'Username and password are required for qBittorrent' }, @@ -77,7 +80,7 @@ export async function POST(request: NextRequest) { disableSSLVerify || false ); } else if (type === 'sabnzbd') { - console.log('[TestDownloadClient] Testing SABnzbd connection'); + logger.debug('Testing SABnzbd connection'); if (!actualPassword) { return NextResponse.json( { success: false, error: 'API key (password) is required for SABnzbd' }, @@ -134,7 +137,7 @@ export async function POST(request: NextRequest) { version, }); } 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( { success: false, diff --git a/src/app/api/admin/settings/test-plex/route.ts b/src/app/api/admin/settings/test-plex/route.ts index 58b56d8..88ab2ee 100644 --- a/src/app/api/admin/settings/test-plex/route.ts +++ b/src/app/api/admin/settings/test-plex/route.ts @@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -70,7 +73,7 @@ export async function POST(request: NextRequest) { })), }); } 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( { success: false, diff --git a/src/app/api/admin/settings/test-prowlarr/route.ts b/src/app/api/admin/settings/test-prowlarr/route.ts index e728c28..8dad05e 100644 --- a/src/app/api/admin/settings/test-prowlarr/route.ts +++ b/src/app/api/admin/settings/test-prowlarr/route.ts @@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -59,7 +62,7 @@ export async function POST(request: NextRequest) { })), }); } 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( { success: false, diff --git a/src/app/api/admin/users/[id]/approve/route.ts b/src/app/api/admin/users/[id]/approve/route.ts index 3575933..6bb6c94 100644 --- a/src/app/api/admin/users/[id]/approve/route.ts +++ b/src/app/api/admin/users/[id]/approve/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Admin.Users.Approve'); export async function POST( request: NextRequest, @@ -64,7 +67,7 @@ export async function POST( }); } } 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( { error: 'Failed to process user approval' }, { status: 500 } diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts index 4d4baad..b0b188a 100644 --- a/src/app/api/admin/users/[id]/route.ts +++ b/src/app/api/admin/users/[id]/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Admin.Users'); export async function PUT( request: NextRequest, @@ -89,7 +92,7 @@ export async function PUT( return NextResponse.json({ user: updatedUser }); } 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( { error: 'Failed to update user' }, { 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.` }); } 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( { error: 'Failed to delete user' }, { status: 500 } diff --git a/src/app/api/admin/users/pending/route.ts b/src/app/api/admin/users/pending/route.ts index 9523710..24b5fd7 100644 --- a/src/app/api/admin/users/pending/route.ts +++ b/src/app/api/admin/users/pending/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; 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) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -30,7 +33,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ users: pendingUsers }); } 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( { error: 'Failed to fetch pending users' }, { status: 500 } diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index 5d79e3d..054a26c 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Admin.Users'); export async function GET(request: NextRequest) { return requireAuth(request, async (req: AuthenticatedRequest) => { @@ -40,7 +43,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ users }); } 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( { error: 'Failed to fetch users' }, { status: 500 } diff --git a/src/app/api/audiobooks/[asin]/route.ts b/src/app/api/audiobooks/[asin]/route.ts index 45783e1..78a3dad 100644 --- a/src/app/api/audiobooks/[asin]/route.ts +++ b/src/app/api/audiobooks/[asin]/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAudibleService } from '@/lib/integrations/audible.service'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Audiobooks.Details'); /** * GET /api/audiobooks/[asin] @@ -45,7 +48,7 @@ export async function GET( audiobook, }); } 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( { error: 'FetchError', diff --git a/src/app/api/audiobooks/covers/route.ts b/src/app/api/audiobooks/covers/route.ts index f6952ff..bbd6b5f 100644 --- a/src/app/api/audiobooks/covers/route.ts +++ b/src/app/api/audiobooks/covers/route.ts @@ -7,6 +7,9 @@ import { NextResponse } from 'next/server'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Audiobooks.Covers'); /** * GET /api/audiobooks/covers?count=100 @@ -64,7 +67,7 @@ export async function GET() { count: shuffled.length, }); } 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 NextResponse.json({ diff --git a/src/app/api/audiobooks/new-releases/route.ts b/src/app/api/audiobooks/new-releases/route.ts index 3c24572..78076de 100644 --- a/src/app/api/audiobooks/new-releases/route.ts +++ b/src/app/api/audiobooks/new-releases/route.ts @@ -9,6 +9,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db'; import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher'; 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 @@ -128,7 +131,7 @@ export async function GET(request: NextRequest) { lastSync: audiobooks[0]?.lastSyncedAt?.toISOString() || null, }); } 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( { error: 'FetchError', diff --git a/src/app/api/audiobooks/popular/route.ts b/src/app/api/audiobooks/popular/route.ts index 7e93fbd..3bab399 100644 --- a/src/app/api/audiobooks/popular/route.ts +++ b/src/app/api/audiobooks/popular/route.ts @@ -9,6 +9,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db'; import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher'; 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 @@ -128,7 +131,7 @@ export async function GET(request: NextRequest) { lastSync: audiobooks[0]?.lastSyncedAt?.toISOString() || null, }); } 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( { error: 'FetchError', diff --git a/src/app/api/audiobooks/request-with-torrent/route.ts b/src/app/api/audiobooks/request-with-torrent/route.ts index f459a59..ffc623e 100644 --- a/src/app/api/audiobooks/request-with-torrent/route.ts +++ b/src/app/api/audiobooks/request-with-torrent/route.ts @@ -11,6 +11,9 @@ import { prisma } from '@/lib/db'; import { getJobQueueService } from '@/lib/services/job-queue.service'; import { findPlexMatch } from '@/lib/utils/audiobook-matcher'; import { z } from 'zod'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.RequestWithTorrent'); const RequestWithTorrentSchema = z.object({ audiobook: z.object({ @@ -153,7 +156,7 @@ export async function POST(request: NextRequest) { } // 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({ where: { id: existingRequest.id }, }); @@ -190,14 +193,14 @@ export async function POST(request: NextRequest) { 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({ success: true, request: newRequest, }, { status: 201 }); } 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) { return NextResponse.json( diff --git a/src/app/api/audiobooks/search-torrents/route.ts b/src/app/api/audiobooks/search-torrents/route.ts index 0fba954..47275a5 100644 --- a/src/app/api/audiobooks/search-torrents/route.ts +++ b/src/app/api/audiobooks/search-torrents/route.ts @@ -10,6 +10,9 @@ import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { getProwlarrService } from '@/lib/integrations/prowlarr.service'; import { rankTorrents } from '@/lib/utils/ranking-algorithm'; import { z } from 'zod'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.AudiobookSearch'); const SearchSchema = z.object({ title: z.string(), @@ -68,14 +71,14 @@ export async function POST(request: NextRequest) { const prowlarr = await getProwlarrService(); 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, { indexerIds: enabledIndexerIds, 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) { return NextResponse.json({ @@ -90,41 +93,30 @@ export async function POST(request: NextRequest) { // No threshold filtering - show all results like interactive search // 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 const top3 = rankedResults.slice(0, 3); if (top3.length > 0) { - console.log(`[AudiobookSearch] ==================== RANKING DEBUG ====================`); - console.log(`[AudiobookSearch] Requested Title: "${title}"`); - console.log(`[AudiobookSearch] Requested Author: "${author}"`); - console.log(`[AudiobookSearch] Top ${top3.length} results (out of ${rankedResults.length} total):`); - console.log(`[AudiobookSearch] --------------------------------------------------------`); + logger.debug('==================== RANKING DEBUG ===================='); + logger.debug('Search parameters', { requestedTitle: title, requestedAuthor: author }); + logger.debug(`Top ${top3.length} results (out of ${rankedResults.length} total)`); + logger.debug('--------------------------------------------------------'); top3.forEach((result, index) => { - console.log(`[AudiobookSearch] ${index + 1}. "${result.title}"`); - console.log(`[AudiobookSearch] Indexer: ${result.indexer}${result.indexerId ? ` (ID: ${result.indexerId})` : ''}`); - console.log(`[AudiobookSearch] `); - console.log(`[AudiobookSearch] Base Score: ${result.score.toFixed(1)}/100`); - console.log(`[AudiobookSearch] - Title/Author Match: ${result.breakdown.matchScore.toFixed(1)}/60`); - console.log(`[AudiobookSearch] - Format Quality: ${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'})`); - console.log(`[AudiobookSearch] `); - console.log(`[AudiobookSearch] Bonus Points: +${result.bonusPoints.toFixed(1)}`); - if (result.bonusModifiers.length > 0) { - result.bonusModifiers.forEach(mod => { - 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] --------------------------------------------------------`); - } + logger.debug(`${index + 1}. "${result.title}"`, { + indexer: result.indexer, + indexerId: result.indexerId, + baseScore: `${result.score.toFixed(1)}/100`, + matchScore: `${result.breakdown.matchScore.toFixed(1)}/60`, + formatScore: `${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`, + seederScore: `${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders !== undefined ? result.seeders + ' seeders' : 'N/A for Usenet'})`, + bonusPoints: `+${result.bonusPoints.toFixed(1)}`, + bonusModifiers: result.bonusModifiers.map(mod => `${mod.reason}: +${mod.points.toFixed(1)}`), + finalScore: result.finalScore.toFixed(1), + notes: result.breakdown.notes, + }); }); - console.log(`[AudiobookSearch] ========================================================`); + logger.debug('========================================================'); } // Add rank position to each result @@ -141,7 +133,7 @@ export async function POST(request: NextRequest) { : 'No results found', }); } 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) { return NextResponse.json( diff --git a/src/app/api/audiobooks/search/route.ts b/src/app/api/audiobooks/search/route.ts index b2bfa8e..4093fcb 100644 --- a/src/app/api/audiobooks/search/route.ts +++ b/src/app/api/audiobooks/search/route.ts @@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAudibleService } from '@/lib/integrations/audible.service'; import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher'; 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 @@ -47,7 +50,7 @@ export async function GET(request: NextRequest) { hasMore: results.hasMore, }); } 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( { error: 'SearchError', diff --git a/src/app/api/auth/admin/login/route.ts b/src/app/api/auth/admin/login/route.ts index cafe63e..291bb20 100644 --- a/src/app/api/auth/admin/login/route.ts +++ b/src/app/api/auth/admin/login/route.ts @@ -8,6 +8,9 @@ import { prisma } from '@/lib/db'; import bcrypt from 'bcrypt'; import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt'; 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 @@ -58,7 +61,7 @@ export async function POST(request: NextRequest) { const decryptedHash = encryptionService.decrypt(user.authToken || ''); passwordValid = await bcrypt.compare(password, decryptedHash); } 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( { error: 'AuthenticationError', @@ -109,7 +112,7 @@ export async function POST(request: NextRequest) { }, }); } 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( { error: 'AuthenticationError', diff --git a/src/app/api/auth/local/login/route.ts b/src/app/api/auth/local/login/route.ts index ed4148a..c5d9d07 100644 --- a/src/app/api/auth/local/login/route.ts +++ b/src/app/api/auth/local/login/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; 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) { 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 result = await provider.handleCallback({ username, password }); if (!result.success) { if (result.requiresApproval) { - console.log('[LocalLogin] Account pending approval:', username); + logger.info('Account pending approval', { username }); return NextResponse.json({ success: false, pendingApproval: true, message: 'Account pending admin approval.', }); } - console.error('[LocalLogin] Login failed:', result.error); + logger.error('Login failed', { error: result.error }); return NextResponse.json( { error: result.error }, { status: 401 } ); } - console.log('[LocalLogin] Login successful for:', username); - console.log('[LocalLogin] User data:', result.user); - console.log('[LocalLogin] Token generated successfully'); + logger.info('Login successful', { username }); + logger.debug('User data', { user: result.user }); + logger.debug('Token generated successfully'); // Return tokens for login return NextResponse.json({ @@ -58,7 +61,7 @@ export async function POST(request: NextRequest) { refreshToken: result.tokens!.refreshToken, }); } catch (error) { - console.error('[LocalLogin] Error:', error); + logger.error('Error', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json( { error: 'Login failed' }, { status: 500 } diff --git a/src/app/api/auth/oidc/callback/route.ts b/src/app/api/auth/oidc/callback/route.ts index 8f748ea..a1c7da6 100644 --- a/src/app/api/auth/oidc/callback/route.ts +++ b/src/app/api/auth/oidc/callback/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAuthProvider } from '@/lib/services/auth'; 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) { const searchParams = request.nextUrl.searchParams; @@ -71,7 +74,7 @@ export async function GET(request: NextRequest) { if (result.isFirstLogin) { // First login - redirect to initializing page to show job progress 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 { // Normal login - redirect to login page with auth success redirectUrl = `${baseUrl}/login?auth=success#authData=${authDataEncoded}`; @@ -132,7 +135,7 @@ export async function GET(request: NextRequest) { return response; } 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'; return NextResponse.redirect(`${baseUrl}/login?error=${encodeURIComponent(errorMsg)}`); diff --git a/src/app/api/auth/oidc/login/route.ts b/src/app/api/auth/oidc/login/route.ts index c6327ca..2d08eef 100644 --- a/src/app/api/auth/oidc/login/route.ts +++ b/src/app/api/auth/oidc/login/route.ts @@ -6,6 +6,9 @@ import { NextResponse } from 'next/server'; import { getAuthProvider } from '@/lib/services/auth'; import { getBaseUrl } from '@/lib/utils/url'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Auth.OIDC.Login'); export async function GET() { try { @@ -25,7 +28,7 @@ export async function GET() { // Redirect to OIDC provider return NextResponse.redirect(redirectUrl); } 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 const baseUrl = getBaseUrl(); diff --git a/src/app/api/auth/plex/callback/route.ts b/src/app/api/auth/plex/callback/route.ts index c8f87a1..c5516b7 100644 --- a/src/app/api/auth/plex/callback/route.ts +++ b/src/app/api/auth/plex/callback/route.ts @@ -9,6 +9,9 @@ import { getEncryptionService } from '@/lib/services/encryption.service'; import { getConfigService } from '@/lib/services/config.service'; import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.PlexCallback'); /** * GET /api/auth/plex/callback?pinId=12345 @@ -52,7 +55,7 @@ export async function GET(request: NextRequest) { // Validate user info if (!plexUser || !plexUser.id) { - console.error('[Plex OAuth] Invalid user info received:', plexUser); + logger.error('Invalid user info received', { plexUser }); return NextResponse.json( { error: 'OAuthError', @@ -64,7 +67,7 @@ export async function GET(request: NextRequest) { } if (!plexUser.username) { - console.error('[Plex OAuth] Username missing from Plex user:', plexUser); + logger.error('Username missing from Plex user', { plexUser }); return NextResponse.json( { error: 'OAuthError', @@ -84,7 +87,7 @@ export async function GET(request: NextRequest) { // Verify server is configured if (!plexConfig.serverUrl || !plexConfig.authToken) { - console.error('[Plex OAuth] Server not configured'); + logger.error('Server not configured'); return NextResponse.json( { error: 'ConfigurationError', @@ -99,7 +102,7 @@ export async function GET(request: NextRequest) { const serverMachineId = plexConfig.machineIdentifier; if (!serverMachineId) { - console.error('[Plex OAuth] machineIdentifier not found in configuration'); + logger.error('machineIdentifier not found in configuration'); return NextResponse.json( { 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 // 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) { - console.warn('[Plex OAuth] User attempted to authenticate without server access:', { + logger.warn('User attempted to authenticate without server access', { plexId: plexIdString, username: plexUser.username, 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 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 // (Only show selection if there's more than just the main account) 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) const accept = request.headers.get('accept') || ''; @@ -157,7 +160,7 @@ export async function GET(request: NextRequest) { (process.env.NODE_ENV === 'production' ? 'https' : 'http'); 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 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 // 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'); const redirectUrl = `${protocol}://${host}/login?auth=success`; - console.log('[Plex OAuth] Setting cookies for mobile auth...'); - console.log('[Plex OAuth] Redirect URL:', redirectUrl); + logger.debug('Setting cookies for mobile auth'); + logger.debug('Redirect URL', { redirectUrl }); // Prepare user data const userDataJson = JSON.stringify({ @@ -260,7 +263,7 @@ export async function GET(request: NextRequest) { role: user.role, 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) const authData = { @@ -331,7 +334,7 @@ export async function GET(request: NextRequest) { path: '/', }); - console.log('[Plex OAuth] Cookies set successfully, returning HTML redirect to:', redirectUrl); + logger.debug('Cookies set successfully, returning HTML redirect', { redirectUrl }); return response; } @@ -351,7 +354,7 @@ export async function GET(request: NextRequest) { }, }); } 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( { error: 'OAuthError', diff --git a/src/app/api/auth/plex/home-users/route.ts b/src/app/api/auth/plex/home-users/route.ts index d2429d3..045e8cf 100644 --- a/src/app/api/auth/plex/home-users/route.ts +++ b/src/app/api/auth/plex/home-users/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; 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 @@ -32,7 +35,7 @@ export async function GET(request: NextRequest) { users, }); } 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( { error: 'ServerError', diff --git a/src/app/api/auth/plex/login/route.ts b/src/app/api/auth/plex/login/route.ts index 4e1507e..385ebf7 100644 --- a/src/app/api/auth/plex/login/route.ts +++ b/src/app/api/auth/plex/login/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; 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 @@ -33,7 +36,7 @@ export async function POST(request: NextRequest) { authUrl, }); } 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( { error: 'OAuthError', diff --git a/src/app/api/auth/plex/switch-profile/route.ts b/src/app/api/auth/plex/switch-profile/route.ts index 7838ed1..e08abb1 100644 --- a/src/app/api/auth/plex/switch-profile/route.ts +++ b/src/app/api/auth/plex/switch-profile/route.ts @@ -8,6 +8,9 @@ import { getPlexService } from '@/lib/integrations/plex.service'; import { getEncryptionService } from '@/lib/services/encryption.service'; import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.PlexSwitchProfile'); /** * POST /api/auth/plex/switch-profile @@ -77,7 +80,7 @@ export async function POST(request: NextRequest) { profileUsername = profileInfo.friendlyName || `User ${userId}`; profileEmail = profileInfo.email || null; profileThumb = profileInfo.thumb || null; - console.log('[Profile Switch] Using provided profile info:', { + logger.debug('Using provided profile info', { plexId: profilePlexId, username: profileUsername, }); @@ -86,7 +89,7 @@ export async function POST(request: NextRequest) { const profileUser = await plexService.getUserInfo(profileToken); 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( { error: 'ServerError', @@ -100,7 +103,7 @@ export async function POST(request: NextRequest) { profileUsername = profileUser.username || `User ${userId}`; profileEmail = profileUser.email || null; profileThumb = profileUser.thumb || null; - console.log('[Profile Switch] Using getUserInfo data:', { + logger.debug('Using getUserInfo data', { plexId: profilePlexId, 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, plexId: user.plexId, username: user.plexUsername, @@ -167,7 +170,7 @@ export async function POST(request: NextRequest) { }, }); } 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( { error: 'ServerError', diff --git a/src/app/api/auth/providers/route.ts b/src/app/api/auth/providers/route.ts index 37e9cb5..994b65b 100644 --- a/src/app/api/auth/providers/route.ts +++ b/src/app/api/auth/providers/route.ts @@ -6,6 +6,9 @@ import { NextResponse } from 'next/server'; import { ConfigurationService } from '@/lib/services/config.service'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Auth.Providers'); export async function GET() { try { @@ -58,7 +61,7 @@ export async function GET() { }); } } 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 const localLoginDisabled = process.env.DISABLE_LOCAL_LOGIN === 'true'; return NextResponse.json({ diff --git a/src/app/api/auth/refresh/route.ts b/src/app/api/auth/refresh/route.ts index 761c05a..f5875c0 100644 --- a/src/app/api/auth/refresh/route.ts +++ b/src/app/api/auth/refresh/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { verifyRefreshToken, generateAccessToken } from '@/lib/utils/jwt'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Auth.Refresh'); /** * POST /api/auth/refresh @@ -68,7 +71,7 @@ export async function POST(request: NextRequest) { expiresIn: 3600, // 1 hour in seconds }); } 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( { error: 'RefreshError', diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index 4e31108..b16f13e 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; 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) const registrationAttempts = new Map(); @@ -74,7 +77,7 @@ export async function POST(request: NextRequest) { refreshToken: result.tokens!.refreshToken, }); } catch (error) { - console.error('[Registration] Error:', error); + logger.error('Registration error', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json( { error: 'Registration failed' }, { status: 500 } diff --git a/src/app/api/bookdate/config/route.ts b/src/app/api/bookdate/config/route.ts index b347349..8f170fb 100644 --- a/src/app/api/bookdate/config/route.ts +++ b/src/app/api/bookdate/config/route.ts @@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; 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) // Any authenticated user can check if BookDate is configured @@ -24,7 +27,7 @@ async function getConfig(req: AuthenticatedRequest) { return NextResponse.json({ config: safeConfig }); } 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( { error: error.message || 'Failed to fetch configuration' }, { status: 500 } @@ -129,7 +132,7 @@ async function saveConfig(req: AuthenticatedRequest) { }); } 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( { error: error.message || 'Failed to save configuration' }, { status: 500 } @@ -162,7 +165,7 @@ async function deleteConfig(req: AuthenticatedRequest) { return NextResponse.json({ success: true }); } 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( { error: error.message || 'Failed to delete configuration' }, { status: 500 } diff --git a/src/app/api/bookdate/generate/route.ts b/src/app/api/bookdate/generate/route.ts index 615ab72..090a686 100644 --- a/src/app/api/bookdate/generate/route.ts +++ b/src/app/api/bookdate/generate/route.ts @@ -14,6 +14,9 @@ import { isAlreadyRequested, isAlreadySwiped, } from '@/lib/bookdate/helpers'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.BookDate.Generate'); async function handler(req: AuthenticatedRequest) { try { @@ -54,7 +57,7 @@ async function handler(req: AuthenticatedRequest) { }; // 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 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'); } - console.log(`[BookDate] AI returned ${aiResponse.recommendations.length} recommendations`); + logger.debug('AI returned recommendations', { count: aiResponse.recommendations.length }); // Match to Audnexus and filter const batchId = `batch_${Date.now()}`; @@ -88,14 +91,14 @@ async function handler(req: AuthenticatedRequest) { const audnexusMatch = await matchToAudnexus(rec.title, rec.author); 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; } // 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)") 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; } @@ -122,12 +125,12 @@ async function handler(req: AuthenticatedRequest) { } } 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; } } - console.log(`[BookDate] Matched ${matched.length} new recommendations`); + logger.info('Matched new recommendations', { count: matched.length }); if (matched.length === 0) { return NextResponse.json( @@ -163,7 +166,7 @@ async function handler(req: AuthenticatedRequest) { }); } catch (error: any) { - console.error('[BookDate] Generate error:', error); + logger.error('Generate error', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json( { error: error.message || 'Failed to generate new recommendations', diff --git a/src/app/api/bookdate/preferences/route.ts b/src/app/api/bookdate/preferences/route.ts index b599a3c..6bf0e51 100644 --- a/src/app/api/bookdate/preferences/route.ts +++ b/src/app/api/bookdate/preferences/route.ts @@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; import { getConfigService } from '@/lib/services/config.service'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.BookDate.Preferences'); /** * GET /api/bookdate/preferences @@ -54,7 +57,7 @@ async function getPreferences(req: AuthenticatedRequest) { }); } 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( { error: error.message || 'Failed to get preferences' }, { status: 500 } @@ -135,7 +138,7 @@ async function updatePreferences(req: AuthenticatedRequest) { }); } 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( { error: error.message || 'Failed to update preferences' }, { status: 500 } diff --git a/src/app/api/bookdate/recommendations/route.ts b/src/app/api/bookdate/recommendations/route.ts index 12264d1..d4617fb 100644 --- a/src/app/api/bookdate/recommendations/route.ts +++ b/src/app/api/bookdate/recommendations/route.ts @@ -14,6 +14,9 @@ import { isAlreadyRequested, isAlreadySwiped, } from '@/lib/bookdate/helpers'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.BookDate.Recommendations'); async function handler(req: AuthenticatedRequest) { try { @@ -75,7 +78,7 @@ async function handler(req: AuthenticatedRequest) { }; // 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 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'); } - console.log(`[BookDate] AI returned ${aiResponse.recommendations.length} recommendations`); + logger.debug('AI returned recommendations', { count: aiResponse.recommendations.length }); // Match to Audnexus and filter const batchId = `batch_${Date.now()}`; @@ -91,19 +94,19 @@ async function handler(req: AuthenticatedRequest) { for (const rec of aiResponse.recommendations) { 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; } // Check if already swiped 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; } // Check if in library 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; } @@ -112,20 +115,20 @@ async function handler(req: AuthenticatedRequest) { const audnexusMatch = await matchToAudnexus(rec.title, rec.author); 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; } // 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)") 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; } // Check if already requested if (await isAlreadyRequested(userId, audnexusMatch.asin)) { - console.log(`[BookDate] Skipping already requested: "${rec.title}"`); + logger.debug('Skipping already requested', { title: rec.title }); continue; } @@ -147,12 +150,12 @@ async function handler(req: AuthenticatedRequest) { } } 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; } } - console.log(`[BookDate] Matched ${matched.length} recommendations`); + logger.info('Matched recommendations', { count: matched.length }); // Save to database if (matched.length > 0) { @@ -180,7 +183,7 @@ async function handler(req: AuthenticatedRequest) { }); } catch (error: any) { - console.error('[BookDate] Recommendations error:', error); + logger.error('Recommendations error', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json( { error: error.message || 'Failed to generate recommendations', diff --git a/src/app/api/bookdate/swipe/route.ts b/src/app/api/bookdate/swipe/route.ts index 9ea923d..9e2dd4d 100644 --- a/src/app/api/bookdate/swipe/route.ts +++ b/src/app/api/bookdate/swipe/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.BookDateSwipe'); async function handler(req: AuthenticatedRequest) { 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) const { getJobQueueService } = await import('@/lib/services/job-queue.service'); @@ -108,11 +111,11 @@ async function handler(req: AuthenticatedRequest) { author: audiobook.author, }); - console.log(`[BookDate] Triggered search job for request ${newRequest.id}`); + logger.info(`Triggered search job for request ${newRequest.id}`); } } 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 } } @@ -124,7 +127,7 @@ async function handler(req: AuthenticatedRequest) { }); } catch (error: any) { - console.error('[BookDate] Swipe error:', error); + logger.error('Swipe error', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json( { error: error.message || 'Failed to record swipe' }, { status: 500 } diff --git a/src/app/api/bookdate/swipes/route.ts b/src/app/api/bookdate/swipes/route.ts index c2b458d..0d85296 100644 --- a/src/app/api/bookdate/swipes/route.ts +++ b/src/app/api/bookdate/swipes/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; 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) async function clearSwipes(req: AuthenticatedRequest) { @@ -16,7 +19,7 @@ async function clearSwipes(req: AuthenticatedRequest) { // Also clear all cached recommendations (since swipe history affects recommendations) 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({ success: true, @@ -24,7 +27,7 @@ async function clearSwipes(req: AuthenticatedRequest) { }); } 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( { error: error.message || 'Failed to clear swipe history' }, { status: 500 } diff --git a/src/app/api/bookdate/test-connection/route.ts b/src/app/api/bookdate/test-connection/route.ts index 65c9296..1c4e6f5 100644 --- a/src/app/api/bookdate/test-connection/route.ts +++ b/src/app/api/bookdate/test-connection/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; 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) { try { @@ -64,7 +67,7 @@ async function authenticatedHandler(req: AuthenticatedRequest) { if (!response.ok) { const errorText = await response.text(); - console.error('[BookDate] OpenAI API error:', errorText); + logger.error('OpenAI API error', { error: errorText }); return NextResponse.json( { error: 'Invalid OpenAI API key or connection failed' }, { status: 400 } @@ -108,7 +111,7 @@ async function authenticatedHandler(req: AuthenticatedRequest) { if (!response.ok) { const errorText = await response.text(); - console.error('[BookDate] Claude API error:', errorText); + logger.error('Claude API error', { error: errorText }); return NextResponse.json( { error: 'Invalid Claude API key or connection failed' }, { status: 400 } @@ -123,7 +126,7 @@ async function authenticatedHandler(req: AuthenticatedRequest) { }); } 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( { error: error.message || 'Connection test failed' }, { status: 500 } @@ -179,7 +182,7 @@ async function unauthenticatedHandler(req: NextRequest) { if (!response.ok) { const errorText = await response.text(); - console.error('[BookDate] OpenAI API error:', errorText); + logger.error('OpenAI API error', { error: errorText }); return NextResponse.json( { error: 'Invalid OpenAI API key or connection failed' }, { status: 400 } @@ -223,7 +226,7 @@ async function unauthenticatedHandler(req: NextRequest) { if (!response.ok) { const errorText = await response.text(); - console.error('[BookDate] Claude API error:', errorText); + logger.error('Claude API error', { error: errorText }); return NextResponse.json( { error: 'Invalid Claude API key or connection failed' }, { status: 400 } @@ -238,7 +241,7 @@ async function unauthenticatedHandler(req: NextRequest) { }); } 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( { error: error.message || 'Connection test failed' }, { status: 500 } diff --git a/src/app/api/bookdate/undo/route.ts b/src/app/api/bookdate/undo/route.ts index c6ab686..1d64cd4 100644 --- a/src/app/api/bookdate/undo/route.ts +++ b/src/app/api/bookdate/undo/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.BookDate.Undo'); async function handler(req: AuthenticatedRequest) { try { @@ -77,7 +80,7 @@ async function handler(req: AuthenticatedRequest) { }); } catch (error: any) { - console.error('[BookDate] Undo error:', error); + logger.error('Undo error', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json( { error: error.message || 'Failed to undo swipe' }, { status: 500 } diff --git a/src/app/api/cache/thumbnails/[filename]/route.ts b/src/app/api/cache/thumbnails/[filename]/route.ts index df2da3f..f1c3348 100644 --- a/src/app/api/cache/thumbnails/[filename]/route.ts +++ b/src/app/api/cache/thumbnails/[filename]/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import fs from 'fs/promises'; import path from 'path'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Thumbnails'); const CACHE_DIR = '/app/cache/thumbnails'; @@ -60,7 +63,7 @@ export async function GET( }, }); } 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( { error: 'Internal server error' }, { status: 500 } diff --git a/src/app/api/config/[category]/route.ts b/src/app/api/config/[category]/route.ts index 86ccd4f..607c60f 100644 --- a/src/app/api/config/[category]/route.ts +++ b/src/app/api/config/[category]/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; 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 export async function GET( @@ -23,7 +26,7 @@ export async function GET( config, }); } 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( { error: 'Failed to get configuration', diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts index 595603e..f6d1e8c 100644 --- a/src/app/api/config/route.ts +++ b/src/app/api/config/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { getConfigService, ConfigUpdate } from '@/lib/services/config.service'; import { z } from 'zod'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Config'); const ConfigUpdateSchema = z.object({ updates: z.array( @@ -35,7 +38,7 @@ export async function PUT(request: NextRequest) { updated: updates.length, }); } 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) { return NextResponse.json( @@ -69,7 +72,7 @@ export async function GET() { config: allConfig, }); } 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( { error: 'Failed to get configuration', diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index 0411d2e..75ad368 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -5,6 +5,9 @@ import { NextResponse } from 'next/server'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Health'); export async function GET() { try { @@ -17,7 +20,7 @@ export async function GET() { database: 'connected', }); } catch (error) { - console.error('Health check failed:', error); + logger.error('Health check failed', { error: error instanceof Error ? error.message : String(error) }); return NextResponse.json( { status: 'unhealthy', diff --git a/src/app/api/init/route.ts b/src/app/api/init/route.ts index 4d964a1..1d64943 100644 --- a/src/app/api/init/route.ts +++ b/src/app/api/init/route.ts @@ -8,25 +8,28 @@ import { NextRequest, NextResponse } from 'next/server'; 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 async function GET(request: NextRequest) { try { - console.log('[Init] Initializing application services...'); + logger.info('Initializing application services...'); // Initialize scheduler service const schedulerService = getSchedulerService(); await schedulerService.start(); - console.log('[Init] Application services initialized successfully'); + logger.info('Application services initialized successfully'); return NextResponse.json({ success: true, message: 'Application services initialized', }); } 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( { diff --git a/src/app/api/requests/[id]/fetch-ebook/route.ts b/src/app/api/requests/[id]/fetch-ebook/route.ts index 05212c7..ff9d241 100644 --- a/src/app/api/requests/[id]/fetch-ebook/route.ts +++ b/src/app/api/requests/[id]/fetch-ebook/route.ts @@ -11,8 +11,9 @@ import { prisma } from '@/lib/db'; import { downloadEbook } from '@/lib/services/ebook-scraper'; import fs from 'fs/promises'; 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) @@ -135,19 +136,21 @@ export async function POST( audiobook.audibleAsin ); - if (DEBUG_ENABLED) { - console.log(`[FetchEbook] Request: ${id}, Title: "${audiobook.title}", Author: "${audiobook.author}"`); - console.log(`[FetchEbook] Target path: ${targetPath}`); - console.log(`[FetchEbook] Config: format=${preferredFormat}, baseUrl=${baseUrl}, flaresolverr=${flaresolverrUrl || 'none'}`); - } + logger.debug('Fetch e-book request', { + requestId: id, + title: audiobook.title, + author: audiobook.author, + targetPath, + format: preferredFormat, + baseUrl, + flaresolverr: flaresolverrUrl || 'none' + }); // Check if target directory exists try { await fs.access(targetPath); } catch { - if (DEBUG_ENABLED) { - console.log(`[FetchEbook] Target directory not found: ${targetPath}`); - } + logger.debug(`Target directory not found: ${targetPath}`); return NextResponse.json( { error: 'Audiobook directory not found. Was the audiobook properly organized?' }, { status: 400 } @@ -167,21 +170,21 @@ export async function POST( ); 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({ success: true, message: `E-book downloaded: ${result.filePath ? path.basename(result.filePath) : 'unknown'}`, format: result.format, }); } 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({ success: false, message: result.error || 'E-book download failed', }); } } 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( { error: error instanceof Error ? error.message : 'Internal server error' }, { status: 500 } diff --git a/src/app/api/requests/[id]/interactive-search/route.ts b/src/app/api/requests/[id]/interactive-search/route.ts index 79f9696..b3bf8b3 100644 --- a/src/app/api/requests/[id]/interactive-search/route.ts +++ b/src/app/api/requests/[id]/interactive-search/route.ts @@ -8,6 +8,9 @@ import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; import { getProwlarrService } from '@/lib/integrations/prowlarr.service'; 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 @@ -96,9 +99,9 @@ export async function POST( // Use custom title if provided, otherwise use audiobook's 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) { - 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, { @@ -106,7 +109,7 @@ export async function POST( 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) { return NextResponse.json({ @@ -125,42 +128,30 @@ export async function POST( // No threshold filtering for interactive search - show all results // 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 const top3 = rankedResults.slice(0, 3); if (top3.length > 0) { - console.log(`[InteractiveSearch] ==================== RANKING DEBUG ====================`); - console.log(`[InteractiveSearch] Search Query: "${searchQuery}"`); - console.log(`[InteractiveSearch] Requested Title (for ranking): "${requestRecord.audiobook.title}"`); - console.log(`[InteractiveSearch] Requested Author (for ranking): "${requestRecord.audiobook.author}"`); - console.log(`[InteractiveSearch] Top ${top3.length} results (out of ${rankedResults.length} total):`); - console.log(`[InteractiveSearch] --------------------------------------------------------`); + logger.debug('==================== RANKING DEBUG ===================='); + logger.debug('Search parameters', { searchQuery, requestedTitle: requestRecord.audiobook.title, requestedAuthor: requestRecord.audiobook.author }); + logger.debug(`Top ${top3.length} results (out of ${rankedResults.length} total)`); + logger.debug('--------------------------------------------------------'); top3.forEach((result, index) => { - console.log(`[InteractiveSearch] ${index + 1}. "${result.title}"`); - console.log(`[InteractiveSearch] Indexer: ${result.indexer}${result.indexerId ? ` (ID: ${result.indexerId})` : ''}`); - console.log(`[InteractiveSearch] `); - console.log(`[InteractiveSearch] Base Score: ${result.score.toFixed(1)}/100`); - console.log(`[InteractiveSearch] - Title/Author Match: ${result.breakdown.matchScore.toFixed(1)}/60`); - console.log(`[InteractiveSearch] - Format Quality: ${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`); - console.log(`[InteractiveSearch] - Seeder Count: ${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders} seeders)`); - console.log(`[InteractiveSearch] `); - console.log(`[InteractiveSearch] Bonus Points: +${result.bonusPoints.toFixed(1)}`); - if (result.bonusModifiers.length > 0) { - result.bonusModifiers.forEach(mod => { - 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] --------------------------------------------------------`); - } + logger.debug(`${index + 1}. "${result.title}"`, { + indexer: result.indexer, + indexerId: result.indexerId, + baseScore: `${result.score.toFixed(1)}/100`, + matchScore: `${result.breakdown.matchScore.toFixed(1)}/60`, + formatScore: `${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`, + seederScore: `${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders} seeders)`, + bonusPoints: `+${result.bonusPoints.toFixed(1)}`, + bonusModifiers: result.bonusModifiers.map(mod => `${mod.reason}: +${mod.points.toFixed(1)}`), + finalScore: result.finalScore.toFixed(1), + notes: result.breakdown.notes, + }); }); - console.log(`[InteractiveSearch] ========================================================`); + logger.debug('========================================================'); } // Add rank position to each result @@ -177,7 +168,7 @@ export async function POST( : 'No results found', }); } 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( { error: 'SearchError', diff --git a/src/app/api/requests/[id]/manual-search/route.ts b/src/app/api/requests/[id]/manual-search/route.ts index c62e957..6147fa4 100644 --- a/src/app/api/requests/[id]/manual-search/route.ts +++ b/src/app/api/requests/[id]/manual-search/route.ts @@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; 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 @@ -89,7 +92,7 @@ export async function POST( message: 'Manual search initiated', }); } 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( { error: 'SearchError', diff --git a/src/app/api/requests/[id]/route.ts b/src/app/api/requests/[id]/route.ts index 8c2d318..2bee262 100644 --- a/src/app/api/requests/[id]/route.ts +++ b/src/app/api/requests/[id]/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.RequestById'); /** * GET /api/requests/[id] @@ -70,7 +73,7 @@ export async function GET( request: requestRecord, }); } 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( { error: 'FetchError', @@ -304,7 +307,7 @@ export async function PATCH( { status: 400 } ); } 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( { error: 'UpdateError', @@ -351,7 +354,7 @@ export async function DELETE( message: 'Request deleted successfully', }); } 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( { error: 'DeleteError', diff --git a/src/app/api/requests/[id]/select-torrent/route.ts b/src/app/api/requests/[id]/select-torrent/route.ts index f774ba5..e759493 100644 --- a/src/app/api/requests/[id]/select-torrent/route.ts +++ b/src/app/api/requests/[id]/select-torrent/route.ts @@ -8,6 +8,9 @@ import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; import { getJobQueueService } from '@/lib/services/job-queue.service'; 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 @@ -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 const jobQueue = getJobQueueService(); @@ -93,7 +96,7 @@ export async function POST( message: 'Torrent download initiated', }); } 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( { error: 'DownloadError', diff --git a/src/app/api/requests/route.ts b/src/app/api/requests/route.ts index f9902fc..f79f4d0 100644 --- a/src/app/api/requests/route.ts +++ b/src/app/api/requests/route.ts @@ -9,6 +9,9 @@ import { prisma } from '@/lib/db'; import { getJobQueueService } from '@/lib/services/job-queue.service'; import { findPlexMatch } from '@/lib/utils/audiobook-matcher'; import { z } from 'zod'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Requests'); const CreateRequestSchema = z.object({ audiobook: z.object({ @@ -138,7 +141,7 @@ export async function POST(request: NextRequest) { } // 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({ where: { id: existingRequest.id }, }); @@ -181,7 +184,7 @@ export async function POST(request: NextRequest) { request: newRequest, }, { status: 201 }); } 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) { return NextResponse.json( @@ -255,7 +258,7 @@ export async function GET(request: NextRequest) { count: requests.length, }); } 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( { error: 'FetchError', diff --git a/src/app/api/setup/complete/route.ts b/src/app/api/setup/complete/route.ts index 7cb3434..f9d7ecb 100644 --- a/src/app/api/setup/complete/route.ts +++ b/src/app/api/setup/complete/route.ts @@ -9,6 +9,9 @@ import bcrypt from 'bcrypt'; import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt'; import { getEncryptionService } from '@/lib/services/encryption.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) { try { @@ -172,12 +175,12 @@ export async function POST(request: NextRequest) { const serverInfo = await plexService.testConnection(plex.url, plex.token); if (serverInfo.success && serverInfo.info?.machineIdentifier) { machineIdentifier = serverInfo.info.machineIdentifier; - console.log('[Setup] Fetched machineIdentifier:', machineIdentifier); + logger.debug('Fetched machineIdentifier', { machineIdentifier }); } else { - console.warn('[Setup] Could not fetch machineIdentifier'); + logger.warn('Could not fetch machineIdentifier'); } } 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) // Note: libraryScope and customPrompt are now per-user settings, not required here 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 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 { - 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 @@ -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 if (adminUser && accessToken && refreshToken) { @@ -530,7 +533,7 @@ export async function POST(request: NextRequest) { }); } } 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( { success: false, diff --git a/src/app/api/setup/status/route.ts b/src/app/api/setup/status/route.ts index e68b04d..9ea7960 100644 --- a/src/app/api/setup/status/route.ts +++ b/src/app/api/setup/status/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Setup.Status'); /** * GET /api/setup/status @@ -24,7 +27,7 @@ export async function GET(request: NextRequest) { }); } catch (error) { // 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({ setupComplete: false, }); diff --git a/src/app/api/setup/test-download-client/route.ts b/src/app/api/setup/test-download-client/route.ts index ccb1f16..99336a5 100644 --- a/src/app/api/setup/test-download-client/route.ts +++ b/src/app/api/setup/test-download-client/route.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { QBittorrentService } from '@/lib/integrations/qbittorrent.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) { try { @@ -80,7 +83,7 @@ export async function POST(request: NextRequest) { { status: 400 } ); } 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( { success: false, diff --git a/src/app/api/setup/test-oidc/route.ts b/src/app/api/setup/test-oidc/route.ts index 23d25fc..0aa0e84 100644 --- a/src/app/api/setup/test-oidc/route.ts +++ b/src/app/api/setup/test-oidc/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { Issuer } from 'openid-client'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Setup.TestOIDC'); export async function POST(request: NextRequest) { try { @@ -65,7 +68,7 @@ export async function POST(request: NextRequest) { }, }); } catch (error) { - console.error('[Test OIDC] Discovery failed:', error); + logger.error('Discovery failed', { error: error instanceof Error ? error.message : String(error) }); // Determine error message let errorMessage = 'OIDC discovery failed'; diff --git a/src/app/api/setup/test-paths/route.ts b/src/app/api/setup/test-paths/route.ts index 01fab3f..2b3a914 100644 --- a/src/app/api/setup/test-paths/route.ts +++ b/src/app/api/setup/test-paths/route.ts @@ -6,21 +6,24 @@ import { NextRequest, NextResponse } from 'next/server'; import fs from 'fs/promises'; import path from 'path'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('API.Setup.TestPaths'); async function testPath(dirPath: string): Promise { try { // Try to access the path try { await fs.access(dirPath); - console.log(`[Setup] Path exists: ${dirPath}`); + logger.debug('Path exists', { path: dirPath }); } catch (accessError) { // 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 { await fs.mkdir(dirPath, { recursive: true }); - console.log(`[Setup] Successfully created path: ${dirPath}`); + logger.debug('Successfully created path', { path: dirPath }); } 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 return false; } @@ -35,7 +38,7 @@ async function testPath(dirPath: string): Promise { return true; } 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; } } @@ -81,7 +84,7 @@ export async function POST(request: NextRequest) { message: 'Directories are ready and writable (created if needed)', }); } 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( { success: false, diff --git a/src/app/api/setup/test-plex/route.ts b/src/app/api/setup/test-plex/route.ts index a762e6a..a5715b2 100644 --- a/src/app/api/setup/test-plex/route.ts +++ b/src/app/api/setup/test-plex/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; 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) { try { @@ -49,7 +52,7 @@ export async function POST(request: NextRequest) { })), }); } 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( { success: false, diff --git a/src/app/api/setup/test-prowlarr/route.ts b/src/app/api/setup/test-prowlarr/route.ts index 15db730..7d70275 100644 --- a/src/app/api/setup/test-prowlarr/route.ts +++ b/src/app/api/setup/test-prowlarr/route.ts @@ -5,6 +5,9 @@ import { NextRequest, NextResponse } from 'next/server'; 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) { try { @@ -38,7 +41,7 @@ export async function POST(request: NextRequest) { })), }); } 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( { success: false, diff --git a/src/lib/bookdate/helpers.ts b/src/lib/bookdate/helpers.ts index dada418..2eb4108 100644 --- a/src/lib/bookdate/helpers.ts +++ b/src/lib/bookdate/helpers.ts @@ -9,6 +9,9 @@ import { getConfigService } from '@/lib/services/config.service'; import { AudibleService } from '@/lib/integrations/audible.service'; import { getPlexService } from '@/lib/integrations/plex.service'; import { findPlexMatch } from '@/lib/utils/audiobook-matcher'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('BookDate'); export interface LibraryBook { title: string; @@ -57,7 +60,7 @@ async function enrichWithUserRatings( }); if (!user) { - console.warn('[BookDate] User not found'); + logger.warn('User not found'); return cachedBooks.map(book => ({ title: book.title, author: book.author, @@ -69,7 +72,7 @@ async function enrichWithUserRatings( // Local admin users: Use cached ratings (from system Plex token) // Local admins authenticate with username/password, not Plex OAuth 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 => ({ title: book.title, author: book.author, @@ -80,10 +83,10 @@ async function enrichWithUserRatings( // 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 - 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) { - console.warn('[BookDate] User has no Plex auth token'); + logger.warn('User has no Plex auth token'); return cachedBooks.map(book => ({ title: book.title, author: book.author, @@ -97,7 +100,7 @@ async function enrichWithUserRatings( const plexConfig = await configService.getPlexConfig(); 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 => ({ title: book.title, author: book.author, @@ -114,7 +117,7 @@ async function enrichWithUserRatings( } catch (decryptError) { // Token might be stored as plain text (from before encryption or different implementation) // 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; } @@ -126,7 +129,7 @@ async function enrichWithUserRatings( // Get server machine ID from stored config (no need to access system token) if (!plexConfig.machineIdentifier) { - console.error('[BookDate] Server machine identifier not configured'); + logger.error('Server machine identifier not configured'); return cachedBooks.map(book => ({ title: book.title, author: book.author, @@ -142,7 +145,7 @@ async function enrichWithUserRatings( ); 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 => ({ title: book.title, 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 const userLibrary = await plexService.getLibraryContent( @@ -160,7 +163,7 @@ async function enrichWithUserRatings( 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 const ratingsMap = new Map(); @@ -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 return cachedBooks.map(book => { @@ -200,10 +203,10 @@ async function enrichWithUserRatings( } catch (fetchError: any) { 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)'); - console.warn('[BookDate] Falling back to recommendations without user ratings'); + logger.warn('User token unauthorized for library access (shared users may not have direct API access)'); + logger.warn('Falling back to recommendations without user ratings'); } 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 return cachedBooks.map(book => ({ @@ -215,7 +218,7 @@ async function enrichWithUserRatings( } } 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 return cachedBooks.map(book => ({ title: book.title, @@ -242,7 +245,7 @@ export async function getUserLibraryBooks( // Early validation: audiobookshelf doesn't support ratings 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'; } @@ -251,7 +254,7 @@ export async function getUserLibraryBooks( if (backendMode === 'audiobookshelf') { const absLibraryId = await configService.get('audiobookshelf.library_id'); if (!absLibraryId) { - console.warn('[BookDate] No Audiobookshelf library ID configured'); + logger.warn('No Audiobookshelf library ID configured'); return []; } libraryId = absLibraryId; @@ -259,7 +262,7 @@ export async function getUserLibraryBooks( // Plex mode const plexConfig = await configService.getPlexConfig(); if (!plexConfig.libraryId) { - console.warn('[BookDate] No Plex library ID configured'); + logger.warn('No Plex library ID configured'); return []; } libraryId = plexConfig.libraryId; @@ -327,7 +330,7 @@ export async function getUserLibraryBooks( } } 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 []; } } @@ -390,8 +393,8 @@ export async function getUserRecentSwipes( (a, b) => b.createdAt.getTime() - a.createdAt.getTime() ); - console.log( - `[BookDate] Fetched ${allSwipes.length} swipes: ${nonDismissSwipes.length} non-dismiss, ${dismissSwipes.length} dismiss` + logger.info( + `Fetched ${allSwipes.length} swipes: ${nonDismissSwipes.length} non-dismiss, ${dismissSwipes.length} dismiss` ); return allSwipes.map((s) => ({ @@ -402,7 +405,7 @@ export async function getUserRecentSwipes( })); } 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 []; } } @@ -424,11 +427,12 @@ export async function buildAIPrompt( const swipeHistory = await getUserRecentSwipes(userId, 10); - console.log('[BookDate] Building AI prompt with context:'); - console.log(`[BookDate] - Library books: ${libraryBooks.length}`); - console.log(`[BookDate] - Swipe history: ${swipeHistory.length}`); - console.log(`[BookDate] - Custom prompt: ${config.customPrompt ? 'Yes' : 'No'}`); - console.log(`[BookDate] - Library scope: ${config.libraryScope}`); + logger.info('Building AI prompt with context:', { + libraryBooks: libraryBooks.length, + swipeHistory: swipeHistory.length, + customPrompt: config.customPrompt ? 'Yes' : 'No', + libraryScope: config.libraryScope, + }); const prompt = { task: 'recommend_audiobooks', @@ -466,7 +470,7 @@ export async function buildAIPrompt( }; const promptString = JSON.stringify(prompt); - console.log('[BookDate] Full AI prompt:', promptString); + logger.debug('Full AI prompt:', { prompt: promptString }); return promptString; } @@ -488,7 +492,7 @@ export async function callAI( const encryptionService = getEncryptionService(); 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') { 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', { method: 'POST', @@ -520,13 +524,13 @@ export async function callAI( if (!response.ok) { 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}`); } const data = await response.json(); const content = data.choices[0].message.content; - console.log('[BookDate] OpenAI response:', content); + logger.debug('OpenAI response:', { content }); return JSON.parse(content); } 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', { method: 'POST', @@ -556,13 +560,13 @@ export async function callAI( if (!response.ok) { 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}`); } const data = await response.json(); 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 const cleanedContent = content @@ -570,7 +574,7 @@ export async function callAI( .replace(/\s*```$/i, '') .trim(); - console.log('[BookDate] Claude cleaned response:', cleanedContent); + logger.debug('Claude cleaned response:', { cleanedContent }); return JSON.parse(cleanedContent); } else { @@ -625,7 +629,7 @@ export async function matchToAudnexus( }); if (cached) { - console.log(`[BookDate] Found in cache: "${cached.title}" by ${cached.author}`); + logger.info(`Found in cache: "${cached.title}" by ${cached.author}`); return { asin: cached.asin, title: cached.title, @@ -638,29 +642,29 @@ export async function matchToAudnexus( } // 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 searchQuery = `${title} ${author}`; const searchResults = await audibleService.search(searchQuery, 1); 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; } // Take the first result (best match) 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) const details = await audibleService.getAudiobookDetails(firstResult.asin); 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; } - console.log(`[BookDate] Successfully matched "${title}" to ASIN ${details.asin}`); + logger.info(`Successfully matched "${title}" to ASIN ${details.asin}`); return { asin: details.asin, @@ -673,7 +677,7 @@ export async function matchToAudnexus( }; } 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; } } @@ -703,12 +707,12 @@ export async function isInLibrary( }); 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; } 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; } } diff --git a/src/lib/integrations/audible.service.ts b/src/lib/integrations/audible.service.ts index 28c7169..1c30499 100644 --- a/src/lib/integrations/audible.service.ts +++ b/src/lib/integrations/audible.service.ts @@ -5,6 +5,10 @@ import axios, { AxiosInstance } from 'axios'; import * as cheerio from 'cheerio'; +import { RMABLogger } from '../utils/logger'; + +// Module-level logger +const logger = RMABLogger.create('Audible'); export interface AudibleAudiobook { asin: string; @@ -48,14 +52,14 @@ export class AudibleService { */ async getPopularAudiobooks(limit: number = 20): Promise { try { - console.log(`[Audible] Fetching popular audiobooks (limit: ${limit})...`); + logger.info(` Fetching popular audiobooks (limit: ${limit})...`); const audiobooks: AudibleAudiobook[] = []; let page = 1; const maxPages = Math.ceil(limit / 20); // Audible shows ~20 items per page 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', { params: page > 1 ? { page } : {}, @@ -105,11 +109,11 @@ export class AudibleService { 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 (foundOnPage < 10) { - console.log(`[Audible] Reached end of available pages`); + logger.info(` Reached end of available pages`); 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; } 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 []; } } @@ -134,14 +138,14 @@ export class AudibleService { */ async getNewReleases(limit: number = 20): Promise { try { - console.log(`[Audible] Fetching new releases (limit: ${limit})...`); + logger.info(` Fetching new releases (limit: ${limit})...`); const audiobooks: AudibleAudiobook[] = []; let page = 1; const maxPages = Math.ceil(limit / 20); // Audible shows ~20 items per page 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', { params: page > 1 ? { page } : {}, @@ -190,11 +194,11 @@ export class AudibleService { 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 (foundOnPage < 10) { - console.log(`[Audible] Reached end of available pages`); + logger.info(` Reached end of available pages`); 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; } 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 []; } } @@ -219,7 +223,7 @@ export class AudibleService { */ async search(query: string, page: number = 1): Promise { try { - console.log(`[Audible] Searching for "${query}"...`); + logger.info(` Searching for "${query}"...`); const response = await this.client.get('/search', { params: { @@ -285,7 +289,7 @@ export class AudibleService { const resultsText = $('.resultsInfo').text().trim(); 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 { query, @@ -295,7 +299,7 @@ export class AudibleService { hasMore: audiobooks.length > 0 && totalResults > page * 20, }; } catch (error) { - console.error('[Audible] Search failed:', error); + logger.error('Search failed', { error: error instanceof Error ? error.message : String(error) }); return { query, results: [], @@ -313,21 +317,21 @@ export class AudibleService { */ async getAudiobookDetails(asin: string): Promise { try { - console.log(`[Audible] Fetching details for ASIN ${asin}...`); + logger.info(` Fetching details for ASIN ${asin}...`); // Try Audnexus first (more reliable) const audnexusData = await this.fetchFromAudnexus(asin); if (audnexusData) { - console.log(`[Audible] Successfully fetched from Audnexus for "${audnexusData.title}"`); + logger.info(` Successfully fetched from Audnexus for "${audnexusData.title}"`); 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 return await this.scrapeAudibleDetails(asin); } 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; } } @@ -337,7 +341,7 @@ export class AudibleService { */ private async fetchFromAudnexus(asin: string): Promise { 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}`, { timeout: 10000, @@ -367,22 +371,22 @@ export class AudibleService { result.coverArtUrl = result.coverArtUrl.replace(/\._.*_\./, '._SL500_.'); } - console.log(`[Audnexus] Success:`, JSON.stringify({ + logger.debug('Audnexus success', { title: result.title, author: result.author, narrator: result.narrator, descLength: result.description?.length || 0, duration: result.durationMinutes, rating: result.rating, - genres: result.genres?.length || 0 - })); + genreCount: result.genres?.length || 0 + }); return result; } catch (error: any) { 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 { - console.log(`[Audnexus] Error fetching ASIN ${asin}:`, error.message); + logger.warn(`Error fetching from Audnexus for ASIN ${asin}`, { error: error.message }); } return null; } @@ -413,20 +417,20 @@ export class AudibleService { const path = require('path'); const debugPath = path.join('/tmp', `audible-${asin}.html`); 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 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) => { try { 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') { - console.log('[Audible] Found valid JSON-LD structured data'); + logger.debug('Found valid JSON-LD structured data'); if (jsonData.name) result.title = jsonData.name; @@ -455,7 +459,7 @@ export class AudibleService { } } } 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() || $('.bc-container 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) @@ -502,7 +506,7 @@ export class AudibleService { } 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) @@ -538,7 +542,7 @@ export class AudibleService { if (result.narrator) { 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 @@ -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 @@ -627,7 +631,7 @@ export class AudibleService { })(); 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 @@ -653,7 +657,7 @@ export class AudibleService { const ratingMatch = ratingText.match(/(\d+\.?\d*)\s*out of/i); 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 @@ -668,7 +672,7 @@ export class AudibleService { if (dateMatch) { 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 @@ -681,23 +685,23 @@ export class AudibleService { }); if (genres.length > 0) { 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}"`); - console.log(`[Audible] Final result:`, JSON.stringify({ + logger.info(`Successfully fetched details for "${result.title}"`); + logger.debug('Final result', { title: result.title, author: result.author, narrator: result.narrator, descLength: result.description?.length || 0, duration: result.durationMinutes, rating: result.rating, - genres: result.genres?.length || 0 - })); + genreCount: result.genres?.length || 0 + }); return result; } 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; } } diff --git a/src/lib/integrations/plex.service.ts b/src/lib/integrations/plex.service.ts index 53b3719..a5cca8a 100644 --- a/src/lib/integrations/plex.service.ts +++ b/src/lib/integrations/plex.service.ts @@ -5,6 +5,10 @@ import axios, { AxiosInstance } from 'axios'; 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_CLIENT_IDENTIFIER = process.env.PLEX_CLIENT_IDENTIFIER || 'readmeabook-unique-client-id'; @@ -106,7 +110,7 @@ export class PlexService { code: response.data.code, }; } 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'); } } @@ -125,7 +129,7 @@ export class PlexService { return response.data.authToken || null; } 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; } } @@ -147,36 +151,36 @@ export class PlexService { // Handle different response formats from Plex if (typeof response.data === 'string') { // XML response - parse it - console.log('[Plex] Received XML response, parsing...'); + logger.debug('Received XML response, parsing...'); const parsed = await parseStringPromise(response.data); // XML attributes are in user.$ if (parsed.user && parsed.user.$) { userData = parsed.user.$; } else { - console.error('[Plex] Unexpected XML structure:', parsed); + logger.error('Unexpected XML structure', { parsed }); throw new Error('Unexpected XML structure in Plex response'); } } else if (response.data && typeof response.data === 'object') { // JSON response - console.log('[Plex] Received JSON response'); + logger.debug('Received JSON response'); userData = response.data; } 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'); } - console.log('[Plex] Parsed user data:', JSON.stringify(userData, null, 2)); + logger.debug('Parsed user data', { userData }); // Validate required fields 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'); } const username = userData.username || userData.title; 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'); } @@ -188,7 +192,7 @@ export class PlexService { authToken, }; } 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) { throw error; // Re-throw our custom errors } @@ -237,7 +241,7 @@ export class PlexService { // 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 = { machineIdentifier: data.machineIdentifier || 'unknown', @@ -252,7 +256,7 @@ export class PlexService { info, }; } catch (error) { - console.error('Plex connection test failed:', error); + logger.error('Connection test failed', { error: error instanceof Error ? error.message : String(error) }); return { success: false, message: 'Could not connect to Plex server. Check server URL and token.', @@ -275,7 +279,7 @@ export class PlexService { userPlexToken: string ): Promise { 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 const response = await this.client.get('https://plex.tv/api/v2/resources', { @@ -300,20 +304,20 @@ export class PlexService { }); 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; } if (!serverResource.accessToken) { - console.error('[Plex] Server resource found but no accessToken provided'); + logger.error('Server resource found but no accessToken provided'); return null; } - console.log('[Plex] Found server access token for:', serverResource.name); + logger.debug('Found server access token', { serverName: serverResource.name }); return serverResource.accessToken; } 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; } } @@ -327,7 +331,7 @@ export class PlexService { */ async verifyServerAccess(serverUrl: string, serverMachineId: string, userToken: string): Promise { 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 const response = await this.client.get('https://plex.tv/api/v2/resources', { @@ -344,21 +348,19 @@ export class PlexService { }); 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 - console.log('[Plex] User accessible resources:', JSON.stringify( - resources.map((r: any) => ({ + logger.debug('User accessible resources', { + resources: resources.map((r: any) => ({ name: r.name, product: r.product, provides: r.provides, clientIdentifier: r.clientIdentifier, machineIdentifier: r.machineIdentifier, owned: r.owned, - })), - null, - 2 - )); + })) + }); // Filter to only server resources (not clients like apps) const servers = resources.filter((r: any) => @@ -367,14 +369,14 @@ export class PlexService { (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 const hasAccess = servers.some((resource: any) => { const resourceId = resource.clientIdentifier || resource.machineIdentifier; const match = resourceId === serverMachineId; - console.log('[Plex] Comparing:', { + logger.debug('Comparing resource', { resourceId, serverMachineId, match, @@ -382,7 +384,7 @@ export class PlexService { }); if (match) { - console.log('[Plex] ✓ Found matching server:', { + logger.debug('Found matching server', { name: resource.name, machineId: resourceId, owned: resource.owned, @@ -393,23 +395,23 @@ export class PlexService { }); if (!hasAccess) { - console.warn('[Plex] ✗ Server not found in user\'s accessible resources'); - console.warn('[Plex] Looking for machineId:', serverMachineId); - console.warn('[Plex] User has access to servers:', - servers.map((r: any) => ({ + logger.warn('Server not found in user accessible resources', { + serverMachineId, + accessibleServers: servers.map((r: any) => ({ name: r.name, clientId: r.clientIdentifier, machineId: r.machineIdentifier, })) - ); + }); } return hasAccess; } catch (error: any) { - console.error('[Plex] Failed to verify server access:', error.response?.status || error.message); - if (error.response?.data) { - console.error('[Plex] Error response:', error.response.data); - } + logger.error('Failed to verify server access', { + status: error.response?.status, + error: error.message, + responseData: error.response?.data + }); return false; } } @@ -456,7 +458,7 @@ export class PlexService { return libraries; } 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'); } } @@ -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 let data = response.data; if (typeof data === 'string') { - console.log('[Plex] Parsing XML response...'); + logger.debug('Parsing XML response...'); const parsed = await parseStringPromise(data); data = parsed.MediaContainer; } else if (data && typeof data === 'object') { // JSON response - could be wrapped in MediaContainer if (data.MediaContainer) { - console.log('[Plex] Extracting from MediaContainer wrapper'); + logger.debug('Extracting from MediaContainer wrapper'); data = data.MediaContainer; } } 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)) { - console.warn('[Plex] tracks is not an array:', tracks); + logger.warn('tracks is not an array', { tracks }); return []; } @@ -527,7 +529,7 @@ export class PlexService { userRating: item.userRating ? parseFloat(item.userRating) : (item.$?.userRating ? parseFloat(item.$?.userRating) : undefined), })); } 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'); } } @@ -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 let data = response.data; if (typeof data === 'string') { - console.log('[Plex] Parsing XML response...'); + logger.debug('Parsing XML response...'); const parsed = await parseStringPromise(data); data = parsed.MediaContainer; } else if (data && typeof data === 'object') { // JSON response - could be wrapped in MediaContainer if (data.MediaContainer) { - console.log('[Plex] Extracting from MediaContainer wrapper'); + logger.debug('Extracting from MediaContainer wrapper'); data = data.MediaContainer; } } - console.log('[Plex] Data structure keys:', Object.keys(data || {})); - console.log('[Plex] Looking for content in: Metadata, Track, Directory, Album'); + logger.debug('Data structure', { keys: Object.keys(data || {}) }); 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)) { - console.warn('[Plex] tracks is not an array:', tracks); + logger.warn('tracks is not an array', { tracks }); return []; } @@ -597,9 +598,9 @@ export class PlexService { })); } catch (error: any) { 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 { - 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'); } @@ -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) { - 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'); } } @@ -665,7 +666,7 @@ export class PlexService { updatedAt: item.updatedAt ? parseInt(item.updatedAt) : Date.now(), })); } 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 []; } } @@ -717,15 +718,15 @@ export class PlexService { } catch (error: any) { // Handle 401 specifically (expired or invalid token) 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; } // Handle 404 (item not found or user doesn't have access) 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; } - 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; } } @@ -765,9 +766,9 @@ export class PlexService { // If we got many 401s, log a warning about token issues 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) { - 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 { try { - console.log('[Plex] Fetching home users from plex.tv/api/home/users'); + logger.debug('Fetching home users'); const response = await this.client.get( 'https://plex.tv/api/home/users', { @@ -792,36 +793,36 @@ export class PlexService { } ); - console.log('[Plex] Home users API response status:', response.status); - console.log('[Plex] Home users API response type:', typeof response.data); + logger.debug('Home users API response', { status: response.status, type: typeof response.data }); // Handle XML response let data = response.data; if (typeof data === 'string') { - console.log('[Plex] Response is XML string, parsing...'); + logger.debug('Response is XML string, parsing...'); const parsed = await parseStringPromise(data); data = parsed; - console.log('[Plex] Parsed XML structure:', JSON.stringify(data, null, 2)); + logger.debug('Parsed XML structure', { data }); } else { - console.log('[Plex] Response is JSON, structure:', JSON.stringify(data, null, 2)); + logger.debug('Response is JSON', { data }); } // Extract users from response // Response structure: { home: { users: [{ user: {...} }] } } or similar const users: any[] = []; - console.log('[Plex] Checking for users in response...'); - console.log('[Plex] data.MediaContainer exists?', !!data.MediaContainer); - console.log('[Plex] data.MediaContainer?.User exists?', !!data.MediaContainer?.User); - console.log('[Plex] data.home exists?', !!data.home); - console.log('[Plex] data.home?.users exists?', !!data.home?.users); - console.log('[Plex] data.users exists?', !!data.users); + logger.debug('Checking for users in response', { + hasMediaContainer: !!data.MediaContainer, + hasMediaContainerUser: !!data.MediaContainer?.User, + hasHome: !!data.home, + hasHomeUsers: !!data.home?.users, + hasUsers: !!data.users + }); // Check for users in MediaContainer.User (XML response structure) 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]; - console.log('[Plex] usersList length:', usersList.length); + logger.debug('usersList length', { count: usersList.length }); usersList.forEach((item: any) => { // XML parsed data has attributes in the $ property if (item.$) { @@ -831,9 +832,9 @@ export class PlexService { } }); } 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]; - console.log('[Plex] usersList length:', usersList.length); + logger.debug('usersList length', { count: usersList.length }); usersList.forEach((item: any) => { if (item.user) { users.push(item.user); @@ -844,9 +845,9 @@ export class PlexService { } }); } 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]; - console.log('[Plex] usersList length:', usersList.length); + logger.debug('usersList length', { count: usersList.length }); usersList.forEach((item: any) => { if (item.user) { users.push(item.user); @@ -857,14 +858,13 @@ export class PlexService { } }); } else { - console.log('[Plex] No users found in expected locations. Full data structure:'); - console.log(JSON.stringify(data, null, 2)); + logger.debug('No users found in expected locations', { data }); } - console.log('[Plex] Extracted', users.length, 'users from response'); + logger.debug('Extracted users from response', { count: users.length }); 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 []; } @@ -898,11 +898,11 @@ export class PlexService { }; }); } catch (error: any) { - console.error('[Plex] Failed to get home users:', error.message || error); - if (error.response) { - console.error('[Plex] Error response status:', error.response.status); - console.error('[Plex] Error response data:', error.response.data); - } + logger.error('Failed to get home users', { + error: error.message || String(error), + status: error.response?.status, + responseData: error.response?.data + }); // Return empty array if no home users (not an error condition) return []; } @@ -958,7 +958,7 @@ export class PlexService { } 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; } @@ -966,10 +966,10 @@ export class PlexService { } catch (error: any) { // Handle PIN errors specifically if (error.response?.status === 401) { - console.error('[Plex] Invalid PIN for profile'); + logger.error('Invalid PIN for profile'); 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'); } } diff --git a/src/lib/integrations/prowlarr.service.ts b/src/lib/integrations/prowlarr.service.ts index 651028e..26be9df 100644 --- a/src/lib/integrations/prowlarr.service.ts +++ b/src/lib/integrations/prowlarr.service.ts @@ -6,6 +6,10 @@ import axios, { AxiosInstance } from 'axios'; import { XMLParser } from 'fast-xml-parser'; import { TorrentResult } from '../utils/ranking-algorithm'; +import { RMABLogger } from '../utils/logger'; + +// Module-level logger +const logger = RMABLogger.create('Prowlarr'); export interface SearchFilters { category?: number; @@ -96,8 +100,7 @@ export class ProwlarrService { // Debug interceptor to log actual outgoing requests this.client.interceptors.request.use((config) => { - console.log(`[Prowlarr] Actual request: ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`); - console.log(`[Prowlarr] Request params:`, JSON.stringify(config.params)); + logger.debug(`Actual request: ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`, { params: config.params }); return config; }); } @@ -130,12 +133,12 @@ export class ProwlarrService { } 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 if (response.data.length > 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 const rawProtocols = response.data.reduce((acc: Record, r: any) => { @@ -143,21 +146,21 @@ export class ProwlarrService { acc[proto] = (acc[proto] || 0) + 1; 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) - if (process.env.LOG_LEVEL === 'debug' && response.data.length > 0) { - console.log('[Prowlarr] Sample raw result from API:', JSON.stringify(response.data[0], null, 2)); + // Debug: Log first raw result full structure (automatically filtered by LOG_LEVEL) + if (response.data.length > 0) { + logger.debug('Sample raw result from API', response.data[0]); } // Transform Prowlarr results to our format const results = response.data .map((result: ProwlarrSearchResult, index: number) => { const transformed = this.transformResult(result); - if (!transformed && process.env.LOG_LEVEL === 'debug') { - // Log the full raw result that was skipped (debug mode only) - console.log(`[Prowlarr] Result #${index + 1} was skipped. Raw data:`, JSON.stringify(result, null, 2)); + if (!transformed) { + // Log the full raw result that was skipped (automatically filtered by LOG_LEVEL) + logger.debug(`Result #${index + 1} was skipped`, { rawData: result }); } return transformed; }) @@ -181,11 +184,11 @@ export class ProwlarrService { 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; } catch (error) { - console.error('Prowlarr search failed:', error); + logger.error('Search failed', { error: error instanceof Error ? error.message : String(error) }); throw new 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'); return response.data; } 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'); } } @@ -213,7 +216,7 @@ export class ProwlarrService { await this.client.get('/health'); return true; } catch (error) { - console.error('Prowlarr connection test failed:', error); + logger.error('Connection test failed', { error: error instanceof Error ? error.message : String(error) }); return false; } } @@ -226,7 +229,7 @@ export class ProwlarrService { const response = await this.client.get('/indexerstats'); return response.data; } 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'); } } @@ -292,7 +295,7 @@ export class ProwlarrService { // Skip torrents without a valid download URL 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; } @@ -315,16 +318,16 @@ export class ProwlarrService { results.push(result); } 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 } } - console.log(`RSS feed for indexer ${indexerId} returned ${results.length} results`); + logger.info(`RSS feed for indexer ${indexerId} returned ${results.length} results`); return results; } 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'}`); } } @@ -340,12 +343,12 @@ export class ProwlarrService { const results = await this.getRssFeed(indexerId); allResults.push(...results); } 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 } } - 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; } @@ -368,33 +371,33 @@ export class ProwlarrService { acc[proto] = (acc[proto] || 0) + 1; return acc; }, {} as Record); - 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 if (results.length > 0 && results.length <= 5) { 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) { - console.log(`[Prowlarr] First 3 results:`); + logger.info(` First 3 results:`); 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') { // Filter for NZB results only 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; } else { // Filter for torrent results only (default) 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; } } 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 } } @@ -435,7 +438,7 @@ export class ProwlarrService { // Validate we have a valid download URL 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; } @@ -464,7 +467,7 @@ export class ProwlarrService { protocol: result.protocol, // 'torrent' or 'usenet' }; } 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; } } @@ -513,7 +516,7 @@ export class ProwlarrService { // Log detected flags for debugging 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; @@ -576,7 +579,7 @@ export async function getProwlarrService(): Promise { // Test connection const isConnected = await prowlarrService.testConnection(); if (!isConnected) { - console.warn('Warning: Prowlarr connection test failed'); + logger.warn('Connection test failed'); } } diff --git a/src/lib/integrations/qbittorrent.service.ts b/src/lib/integrations/qbittorrent.service.ts index d0d3172..4081a3d 100644 --- a/src/lib/integrations/qbittorrent.service.ts +++ b/src/lib/integrations/qbittorrent.service.ts @@ -7,10 +7,14 @@ import axios, { AxiosInstance } from 'axios'; import https from 'https'; import * as parseTorrentModule from 'parse-torrent'; import FormData from 'form-data'; +import { RMABLogger } from '../utils/logger'; // Handle both ESM and CommonJS imports const parseTorrent = (parseTorrentModule as any).default || parseTorrentModule; +// Module-level logger +const logger = RMABLogger.create('QBittorrent'); + export interface AddTorrentOptions { savePath?: string; category?: string; @@ -104,7 +108,7 @@ export class QBittorrentService { this.httpsAgent = new https.Agent({ rejectUnauthorized: false, }); - console.log('[qBittorrent] SSL certificate verification disabled'); + logger.info('[QBittorrent] SSL certificate verification disabled'); } this.client = axios.create({ @@ -126,7 +130,11 @@ export class QBittorrentService { 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, } ); @@ -141,9 +149,9 @@ export class QBittorrentService { throw new Error('Failed to authenticate with qBittorrent'); } - console.log('Successfully authenticated with qBittorrent'); + logger.info('Successfully authenticated'); } 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'); } } @@ -154,7 +162,7 @@ export class QBittorrentService { async addTorrent(url: string, options?: AddTorrentOptions): Promise { // Validate URL parameter 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'); } @@ -171,21 +179,21 @@ export class QBittorrentService { // Determine if this is a magnet link or .torrent file URL if (url.startsWith('magnet:')) { - console.log('[qBittorrent] Detected magnet link'); + logger.info('[QBittorrent] Detected magnet link'); return await this.addMagnetLink(url, category, options); } else { - console.log('[qBittorrent] Detected .torrent file URL'); + logger.info('[QBittorrent] Detected .torrent file URL'); return await this.addTorrentFile(url, category, options); } } catch (error) { // Try re-authenticating if we get a 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(); 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'); } } @@ -205,12 +213,12 @@ export class QBittorrentService { 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 try { 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; } catch { // Torrent doesn't exist, continue with adding @@ -229,7 +237,7 @@ export class QBittorrentService { 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, { headers: { @@ -242,7 +250,7 @@ export class QBittorrentService { 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; } @@ -254,7 +262,7 @@ export class QBittorrentService { category: string, options?: AddTorrentOptions ): Promise { - console.log(`[qBittorrent] Downloading .torrent file from: ${torrentUrl}`); + logger.info(` Downloading .torrent file from: ${torrentUrl}`); // Make initial request with maxRedirects: 0 to intercept redirects // 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 }); - 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 if (torrentResponse.data.length > 0) { const responseText = torrentResponse.data.toString(); const magnetMatch = responseText.match(/^magnet:\?[^\s]+$/); 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); } } @@ -283,7 +291,7 @@ export class QBittorrentService { } catch (error) { if (!axios.isAxiosError(error) || !error.response) { // 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; } @@ -292,26 +300,26 @@ export class QBittorrentService { // Handle 3xx redirects if (status >= 300 && status < 400) { 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 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); } // Regular HTTP redirect - follow it manually if (location && (location.startsWith('http://') || location.startsWith('https://'))) { - console.log(`[qBittorrent] Following HTTP redirect...`); + logger.info(` Following HTTP redirect...`); try { torrentResponse = await axios.get(location, { responseType: 'arraybuffer', timeout: 30000, 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) { - 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'); } } else { @@ -319,20 +327,20 @@ export class QBittorrentService { } } else { // 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}`); } } 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) let parsedTorrent: any; try { parsedTorrent = await parseTorrent(torrentBuffer); } 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'); } @@ -342,13 +350,13 @@ export class QBittorrentService { throw new Error('Failed to extract info_hash from .torrent file'); } - console.log(`[qBittorrent] Extracted info_hash: ${infoHash}`); - console.log(`[qBittorrent] Torrent name: ${parsedTorrent.name || 'Unknown'}`); + logger.info(` Extracted info_hash: ${infoHash}`); + logger.info(` Torrent name: ${parsedTorrent.name || 'Unknown'}`); // Check for duplicates try { 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; } catch { // Torrent doesn't exist, continue with adding @@ -371,7 +379,7 @@ export class QBittorrentService { 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, { headers: { @@ -386,7 +394,7 @@ export class QBittorrentService { throw new Error(`qBittorrent rejected .torrent file: ${response.data}`); } - console.log(`[qBittorrent] Successfully added torrent: ${infoHash}`); + logger.info(` Successfully added torrent: ${infoHash}`); return infoHash; } @@ -410,7 +418,7 @@ export class QBittorrentService { if (!existingCategory) { // 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( '/torrents/createCategory', @@ -426,13 +434,13 @@ export class QBittorrentService { } ); - console.log(`[qBittorrent] Category "${category}" created successfully`); + logger.info(` Category "${category}" created successfully`); } else { // Category exists - check if save path needs updating const currentSavePath = existingCategory.savePath || existingCategory.save_path; 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( '/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 { - 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) { // If we can't ensure the category, log error but don't throw // Torrents can still be added with per-torrent savepath parameter if (axios.isAxiosError(error)) { - console.error(`[qBittorrent] Failed to ensure category "${category}":`, { + logger.error(` Failed to ensure category "${category}":`, { status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, requestedPath: this.defaultSavePath, }); } 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; } 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'); } } @@ -541,9 +549,9 @@ export class QBittorrentService { } ); - console.log(`Paused torrent: ${hash}`); + logger.info(`Paused torrent: ${hash}`); } 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'); } } @@ -568,9 +576,9 @@ export class QBittorrentService { } ); - console.log(`Resumed torrent: ${hash}`); + logger.info(`Resumed torrent: ${hash}`); } 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'); } } @@ -598,9 +606,9 @@ export class QBittorrentService { } ); - console.log(`Deleted torrent: ${hash}`); + logger.info(`Deleted torrent: ${hash}`); } 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'); } } @@ -621,7 +629,7 @@ export class QBittorrentService { return response.data; } 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'); } } @@ -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) { - 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'); } } @@ -664,7 +672,7 @@ export class QBittorrentService { await this.login(); return true; } catch (error) { - console.error('qBittorrent connection test failed:', error); + logger.error('Connection test failed', { error: error instanceof Error ? error.message : String(error) }); return false; } } @@ -686,7 +694,7 @@ export class QBittorrentService { httpsAgent = new https.Agent({ rejectUnauthorized: false, }); - console.log('[qBittorrent] SSL certificate verification disabled for test connection'); + logger.info('[QBittorrent] SSL certificate verification disabled for test connection'); } try { @@ -694,7 +702,11 @@ export class QBittorrentService { `${baseUrl}/api/v2/auth/login`, 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, } ); @@ -714,7 +726,7 @@ export class QBittorrentService { return versionResponse.data || 'Connected'; } 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 if (axios.isAxiosError(error)) { @@ -856,7 +868,7 @@ let configLoaded = false; * Forces service to reload configuration from database on next use */ 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; configLoaded = false; } @@ -869,7 +881,7 @@ export async function getQBittorrentService(): Promise { const { getConfigService } = await import('@/lib/services/config.service'); const configService = getConfigService(); - console.log('[qBittorrent] Loading configuration from database...'); + logger.info('[QBittorrent] Loading configuration from database...'); const config = await configService.getMany([ 'download_client_url', 'download_client_username', @@ -878,7 +890,7 @@ export async function getQBittorrentService(): Promise { 'download_client_disable_ssl_verify', ]); - console.log('[qBittorrent] Config loaded:', { + logger.info('[QBittorrent] Config loaded:', { hasUrl: !!config.download_client_url, hasUsername: !!config.download_client_username, hasPassword: !!config.download_client_password, @@ -904,7 +916,7 @@ export async function getQBittorrentService(): Promise { if (missingFields.length > 0) { 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); } @@ -915,7 +927,7 @@ export async function getQBittorrentService(): Promise { const savePath = config.download_dir as string; const disableSSLVerify = config.download_client_disable_ssl_verify === 'true'; - console.log('[qBittorrent] Creating service instance...'); + logger.info('[QBittorrent] Creating service instance...'); qbittorrentService = new QBittorrentService( url, username, @@ -926,17 +938,17 @@ export async function getQBittorrentService(): Promise { ); // Test connection - console.log('[qBittorrent] Testing connection...'); + logger.info('[QBittorrent] Testing connection...'); const isConnected = await qbittorrentService.testConnection(); 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.'); } else { - console.log('[qBittorrent] Connection test successful'); + logger.info('[QBittorrent] Connection test successful'); configLoaded = true; // Mark as successfully loaded } } 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 configLoaded = false; throw error; diff --git a/src/lib/integrations/sabnzbd.service.ts b/src/lib/integrations/sabnzbd.service.ts index 3b3be4b..6053113 100644 --- a/src/lib/integrations/sabnzbd.service.ts +++ b/src/lib/integrations/sabnzbd.service.ts @@ -5,6 +5,9 @@ import axios, { AxiosInstance } from 'axios'; import https from 'https'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('SABnzbd'); export interface AddNZBOptions { category?: string; @@ -238,7 +241,7 @@ export class SABnzbdService { const categoryExists = config.categories.some(cat => cat.name === this.defaultCategory); if (!categoryExists) { - console.log(`[SABnzbd] Creating category: ${this.defaultCategory}`); + logger.info(`Creating category: ${this.defaultCategory}`); // Create category 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 { - console.log(`[SABnzbd] Category already exists: ${this.defaultCategory}`); + logger.info(`Category already exists: ${this.defaultCategory}`); } } 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 } } @@ -267,7 +270,7 @@ export class SABnzbdService { * Returns the NZB ID */ async addNZB(url: string, options?: AddNZBOptions): Promise { - 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', { params: { @@ -291,7 +294,7 @@ export class SABnzbdService { } const nzbId = nzbIds[0]; - console.log(`[SABnzbd] Added NZB: ${nzbId}`); + logger.info(`Added NZB: ${nzbId}`); return nzbId; } @@ -559,5 +562,5 @@ export async function getSABnzbdService(): Promise { export function invalidateSABnzbdService(): void { sabnzbdServiceInstance = null; - console.log('[SABnzbd] Service singleton invalidated'); + logger.info('Service singleton invalidated'); } diff --git a/src/lib/middleware/auth.ts b/src/lib/middleware/auth.ts index 8e634d3..be6b6f9 100644 --- a/src/lib/middleware/auth.ts +++ b/src/lib/middleware/auth.ts @@ -6,6 +6,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { verifyAccessToken, TokenPayload } from '../utils/jwt'; import { prisma } from '../db'; +import { RMABLogger } from '../utils/logger'; + +const logger = RMABLogger.create('Auth'); export interface AuthenticatedRequest extends NextRequest { user?: TokenPayload & { id: string }; @@ -40,7 +43,7 @@ export async function requireAuth( const token = extractToken(request); if (!token) { - console.error('[Auth Middleware] No token provided'); + logger.error('No token provided'); return NextResponse.json( { error: 'Unauthorized', @@ -53,7 +56,7 @@ export async function requireAuth( const payload = verifyAccessToken(token); if (!payload) { - console.error('[Auth Middleware] Token verification failed'); + logger.error('Token verification failed'); return NextResponse.json( { error: 'Unauthorized', @@ -69,7 +72,7 @@ export async function requireAuth( }); 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( { error: 'Unauthorized', diff --git a/src/lib/processors/audible-refresh.processor.ts b/src/lib/processors/audible-refresh.processor.ts index 6e43222..150e52b 100644 --- a/src/lib/processors/audible-refresh.processor.ts +++ b/src/lib/processors/audible-refresh.processor.ts @@ -6,7 +6,7 @@ */ import { prisma } from '../db'; -import { createJobLogger } from '../utils/job-logger'; +import { RMABLogger } from '../utils/logger'; export interface AudibleRefreshPayload { jobId?: string; @@ -15,9 +15,9 @@ export interface AudibleRefreshPayload { export async function processAudibleRefresh(payload: AudibleRefreshPayload): Promise { 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 { getThumbnailCacheService } = await import('../services/thumbnail-cache.service'); @@ -40,13 +40,13 @@ export async function processAudibleRefresh(payload: AudibleRefreshPayload): Pro 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 const popular = await audibleService.getPopularAudiobooks(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 let popularSaved = 0; @@ -99,7 +99,7 @@ export async function processAudibleRefresh(payload: AudibleRefreshPayload): Pro popularSaved++; } 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++; } 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 - await logger?.info('Cleaning up unused thumbnails...'); + logger.info('Cleaning up unused thumbnails...'); const allActiveAsins = await prisma.audibleCache.findMany({ select: { asin: true }, }); const activeAsinSet = new Set(allActiveAsins.map(item => item.asin)); const deletedCount = await thumbnailCache.cleanupUnusedThumbnails(activeAsinSet); - await logger?.info(`Cleanup complete: ${deletedCount} unused thumbnails removed`); + logger.info(`Cleanup complete: ${deletedCount} unused thumbnails removed`); return { success: true, @@ -172,7 +172,7 @@ export async function processAudibleRefresh(payload: AudibleRefreshPayload): Pro thumbnailsDeleted: deletedCount, }; } 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; } } diff --git a/src/lib/processors/cleanup-seeded-torrents.processor.ts b/src/lib/processors/cleanup-seeded-torrents.processor.ts index 8066e4a..0ec6cd5 100644 --- a/src/lib/processors/cleanup-seeded-torrents.processor.ts +++ b/src/lib/processors/cleanup-seeded-torrents.processor.ts @@ -6,7 +6,7 @@ */ import { prisma } from '../db'; -import { createJobLogger } from '../utils/job-logger'; +import { RMABLogger } from '../utils/logger'; export interface CleanupSeededTorrentsPayload { jobId?: string; @@ -15,9 +15,9 @@ export interface CleanupSeededTorrentsPayload { export async function processCleanupSeededTorrents(payload: CleanupSeededTorrentsPayload): Promise { 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 { // 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'); if (!indexersConfigStr) { - await logger?.warn('No indexer configuration found, skipping'); + logger.warn('No indexer configuration found, skipping'); return { success: false, message: 'No indexer configuration', @@ -42,7 +42,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent 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) // 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 }); - 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 skipped = 0; @@ -95,7 +95,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent // For soft-deleted SABnzbd requests, hard delete immediately (no seeding needed) if (request.deletedAt) { 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; } @@ -116,7 +116,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent // For soft-deleted requests with unlimited seeding, hard delete immediately if (request.deletedAt) { 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++; continue; @@ -146,7 +146,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent 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 // 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) { - 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 (request.deletedAt) { 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++; @@ -183,18 +183,18 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent // If this is a soft-deleted request (orphaned download), hard delete it now if (request.deletedAt) { 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 { - await logger?.info(`Deleted torrent and files for active request ${request.id}`); + logger.info(`Deleted torrent and files for active request ${request.id}`); } cleaned++; } 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 { success: true, @@ -205,7 +205,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent unlimited: noConfig, }; } 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; } } diff --git a/src/lib/processors/download-torrent.processor.ts b/src/lib/processors/download-torrent.processor.ts index 0be4709..f17aafa 100644 --- a/src/lib/processors/download-torrent.processor.ts +++ b/src/lib/processors/download-torrent.processor.ts @@ -8,7 +8,7 @@ import { prisma } from '../db'; import { getQBittorrentService } from '../integrations/qbittorrent.service'; import { getSABnzbdService } from '../integrations/sabnzbd.service'; import { getConfigService } from '../services/config.service'; -import { createJobLogger } from '../utils/job-logger'; +import { RMABLogger } from '../utils/logger'; /** * Process download job @@ -18,10 +18,10 @@ import { createJobLogger } from '../utils/job-logger'; export async function processDownloadTorrent(payload: DownloadTorrentPayload): Promise { 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}"`); - await logger?.info(`Selected result: ${torrent.title}`, { + logger.info(`Processing request ${requestId} for "${audiobook.title}"`); + logger.info(`Selected result: ${torrent.title}`, { size: torrent.size, seeders: torrent.seeders, format: torrent.format, @@ -48,7 +48,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P if (clientType === 'sabnzbd') { // Route to SABnzbd - await logger?.info(`Routing to SABnzbd`); + logger.info(`Routing to SABnzbd`); const sabnzbd = await getSABnzbdService(); downloadClientId = await sabnzbd.addNZB(torrent.downloadUrl, { @@ -57,7 +57,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P }); downloadClient = 'sabnzbd'; - await logger?.info(`NZB added with ID: ${downloadClientId}`); + logger.info(`NZB added with ID: ${downloadClientId}`); // Create DownloadHistory record 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 const jobQueue = getJobQueueService(); @@ -91,7 +91,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P 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 { success: true, @@ -107,7 +107,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P }; } else { // Route to qBittorrent (default) - await logger?.info(`Routing to qBittorrent`); + logger.info(`Routing to qBittorrent`); const qbt = await getQBittorrentService(); downloadClientId = await qbt.addTorrent(torrent.downloadUrl, { @@ -118,7 +118,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P }); downloadClient = 'qbittorrent'; - await logger?.info(`Torrent added with hash: ${downloadClientId}`); + logger.info(`Torrent added with hash: ${downloadClientId}`); // Create DownloadHistory record 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 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 ); - 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 { success: true, @@ -169,7 +169,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P }; } } 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 await prisma.request.update({ diff --git a/src/lib/processors/match-plex.processor.ts b/src/lib/processors/match-plex.processor.ts index 8726096..dea34d4 100644 --- a/src/lib/processors/match-plex.processor.ts +++ b/src/lib/processors/match-plex.processor.ts @@ -11,7 +11,7 @@ import { prisma } from '../db'; import { getLibraryService } from '../services/library'; import { compareTwoStrings } from 'string-similarity'; 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) @@ -20,10 +20,10 @@ import { createJobLogger } from '../utils/job-logger'; export async function processMatchPlex(payload: MatchPlexPayload): Promise { 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.'); - await logger?.info(`Matching "${title}" by ${author} in library`); + logger.warn('DEPRECATED: match_plex job is deprecated. Use scan_plex instead.'); + logger.info(`Matching "${title}" by ${author} in library`); try { // Get library service and configuration @@ -31,7 +31,7 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise const libraryService = await getLibraryService(); const backendMode = await configService.getBackendMode(); - await logger?.info(`Backend mode: ${backendMode}`); + logger.info(`Backend mode: ${backendMode}`); // Get configured library ID const libraryId = backendMode === 'audiobookshelf' @@ -45,10 +45,10 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise // Search library using abstraction layer 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) { - 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 await prisma.request.update({ @@ -92,7 +92,7 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise 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), titleScore: Math.round(bestMatch.titleScore * 100), authorScore: Math.round(bestMatch.authorScore * 100), @@ -100,7 +100,7 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise // Accept match if score >= 70% if (bestMatch.score >= 0.7) { - await logger?.info(`Match accepted!`); + logger.info(`Match accepted!`); // Update audiobook with library item ID const updateData: any = { @@ -144,7 +144,7 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise }, }; } 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 await prisma.request.update({ @@ -166,7 +166,7 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise }; } } 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 // Just log the error and mark as completed diff --git a/src/lib/processors/monitor-download.processor.ts b/src/lib/processors/monitor-download.processor.ts index 4d74dde..b697a5b 100644 --- a/src/lib/processors/monitor-download.processor.ts +++ b/src/lib/processors/monitor-download.processor.ts @@ -7,7 +7,7 @@ import path from 'path'; import { MonitorDownloadPayload, getJobQueueService } from '../services/job-queue.service'; import { prisma } from '../db'; 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 { getConfigService } from '../services/config.service'; @@ -18,7 +18,7 @@ import { getConfigService } from '../services/config.service'; async function getTorrentWithRetry( qbt: any, hash: string, - logger: JobLogger | null, + logger: RMABLogger, maxRetries: number = 3, initialDelayMs: number = 500 ): Promise { @@ -37,7 +37,7 @@ async function getTorrentWithRetry( // Exponential backoff: 500ms, 1000ms, 2000ms 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)); } @@ -55,7 +55,7 @@ async function getTorrentWithRetry( export async function processMonitorDownload(payload: MonitorDownloadPayload): Promise { const { requestId, downloadHistoryId, downloadClientId, downloadClient, jobId } = payload; - const logger = jobId ? createJobLogger(jobId, 'MonitorDownload') : null; + const logger = RMABLogger.forJob(jobId, 'MonitorDownload'); try { let progress: any; @@ -96,7 +96,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P // Store download path if available (only set after completion) downloadPath = nzbInfo.downloadPath; - await logger?.info(`SABnzbd status: ${nzbInfo.status}`, { + logger.info(`SABnzbd status: ${nzbInfo.status}`, { progress: `${(nzbInfo.progress * 100).toFixed(1)}%`, speed: `${(nzbInfo.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s`, }); @@ -123,7 +123,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P // Check download state 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 if (!downloadPath) { @@ -145,7 +145,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P localPath: pathMappingConfig.download_client_local_path || '', }); - await logger?.info(`Download completed`, { + logger.info(`Download completed`, { downloadClient, downloadPath, organizePath: organizePath !== downloadPath ? `${organizePath} (mapped)` : organizePath, @@ -183,7 +183,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P organizePath ); - await logger?.info(`Triggered organize_files job for request ${requestId}`); + logger.info(`Triggered organize_files job for request ${requestId}`); return { success: true, @@ -194,7 +194,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P downloadPath: organizePath, }; } else if (progress.state === 'failed') { - await logger?.error(`Download failed for request ${requestId}`); + logger.error(`Download failed for request ${requestId}`); // Update request to failed await prisma.request.update({ @@ -236,7 +236,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P // Only log every 5% progress to reduce log spam const shouldLog = progress.percent % 5 === 0 || progress.percent < 5; if (shouldLog) { - await logger?.info(`Request ${requestId}: ${progress.percent}% complete (${progress.state})`, { + logger.info(`Request ${requestId}: ${progress.percent}% complete (${progress.state})`, { speed: progress.speed, eta: progress.eta, }); @@ -254,7 +254,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P }; } } 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 const errorMessage = error instanceof Error ? error.message : ''; @@ -263,7 +263,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P if (isTorrentNotFound) { // Transient error - don't mark request as failed, let Bull retry // 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 { // Permanent error - mark request as failed immediately await prisma.request.update({ diff --git a/src/lib/processors/monitor-rss-feeds.processor.ts b/src/lib/processors/monitor-rss-feeds.processor.ts index 91ef0ac..fe96883 100644 --- a/src/lib/processors/monitor-rss-feeds.processor.ts +++ b/src/lib/processors/monitor-rss-feeds.processor.ts @@ -6,7 +6,7 @@ */ import { prisma } from '../db'; -import { createJobLogger } from '../utils/job-logger'; +import { RMABLogger } from '../utils/logger'; import { getJobQueueService } from '../services/job-queue.service'; export interface MonitorRssFeedsPayload { @@ -16,9 +16,9 @@ export interface MonitorRssFeedsPayload { export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): Promise { 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 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'); if (!indexersConfigStr) { - await logger?.warn(`No indexers configured, skipping`); + logger.warn(`No indexers configured, skipping`); return { success: false, message: 'No indexers configured', skipped: true }; } @@ -38,11 +38,11 @@ export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): P ); 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 }; } - await logger?.info(`Monitoring ${rssEnabledIndexers.length} RSS-enabled indexers`); + logger.info(`Monitoring ${rssEnabledIndexers.length} RSS-enabled indexers`); // Get RSS feeds from all enabled indexers 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 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) { return { success: true, message: 'No RSS results', matched: 0 }; @@ -67,7 +67,7 @@ export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): P take: 100, }); - await logger?.info(`Found ${missingRequests.length} requests awaiting search`); + logger.info(`Found ${missingRequests.length} requests awaiting search`); if (missingRequests.length === 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; 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 try { @@ -102,9 +102,9 @@ export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): P author: audiobook.author, }); matched++; - await logger?.info(`Triggered search job for request ${request.id}`); + logger.info(`Triggered search job for request ${request.id}`); } 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 @@ -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 { success: true, diff --git a/src/lib/processors/organize-files.processor.ts b/src/lib/processors/organize-files.processor.ts index 6b5d16a..b8fb188 100644 --- a/src/lib/processors/organize-files.processor.ts +++ b/src/lib/processors/organize-files.processor.ts @@ -6,7 +6,7 @@ import { OrganizeFilesPayload, getJobQueueService } from '../services/job-queue.service'; import { prisma } from '../db'; import { getFileOrganizer } from '../utils/file-organizer'; -import { createJobLogger } from '../utils/job-logger'; +import { RMABLogger } from '../utils/logger'; import { getLibraryService } from '../services/library'; import { getConfigService } from '../services/config.service'; @@ -17,11 +17,10 @@ import { getConfigService } from '../services/config.service'; export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promise { const { requestId, audiobookId, downloadPath, jobId } = payload; - // Create logger (fallback to console-only if jobId not provided) - const logger = jobId ? createJobLogger(jobId, 'OrganizeFiles') : null; + const logger = RMABLogger.forJob(jobId, 'OrganizeFiles'); - await logger?.info(`Processing request ${requestId}`); - await logger?.info(`Download path: ${downloadPath}`); + logger.info(`Processing request ${requestId}`); + logger.info(`Download path: ${downloadPath}`); try { // Update request status to processing @@ -43,7 +42,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi 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) const organizer = await getFileOrganizer(); @@ -65,7 +64,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi 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 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, message: 'Files organized successfully', requestId, @@ -128,13 +127,13 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi // Trigger scan (implementation is backend-specific) await libraryService.triggerLibraryScan(libraryId); - await logger?.info( + logger.info( `Triggered ${backendMode} filesystem scan for library ${libraryId}` ); } catch (error) { // 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'}`, { 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 } } else { - await logger?.info( + logger.info( `${backendMode} filesystem scan trigger disabled (relying on filesystem watcher)` ); } @@ -161,7 +160,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi errors: result.errors, }; } 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'; @@ -191,7 +190,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi if (newAttempts < currentRequest.maxImportRetries) { // 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({ where: { id: requestId }, @@ -213,7 +212,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi }; } else { // 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({ where: { id: requestId }, diff --git a/src/lib/processors/plex-recently-added.processor.ts b/src/lib/processors/plex-recently-added.processor.ts index 6ba279f..6dbde25 100644 --- a/src/lib/processors/plex-recently-added.processor.ts +++ b/src/lib/processors/plex-recently-added.processor.ts @@ -6,7 +6,7 @@ */ import { prisma } from '../db'; -import { createJobLogger } from '../utils/job-logger'; +import { RMABLogger } from '../utils/logger'; import { getLibraryService } from '../services/library'; export interface PlexRecentlyAddedPayload { @@ -16,14 +16,14 @@ export interface PlexRecentlyAddedPayload { export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPayload): Promise { 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 configService = getConfigService(); // Get backend mode const backendMode = await configService.getBackendMode(); - await logger?.info(`Backend mode: ${backendMode}`); + logger.info(`Backend mode: ${backendMode}`); // Validate configuration based on backend mode if (backendMode === 'audiobookshelf') { @@ -40,7 +40,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa if (missingFields.length > 0) { const errorMsg = `Audiobookshelf is not configured. Missing: ${missingFields.join(', ')}`; - await logger?.warn(errorMsg); + logger.warn(errorMsg); return { success: false, message: errorMsg, skipped: true }; } } else { @@ -57,12 +57,12 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa if (missingFields.length > 0) { const errorMsg = `Plex is not configured. Missing: ${missingFields.join(', ')}`; - await logger?.warn(errorMsg); + logger.warn(errorMsg); 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) const libraryService = await getLibraryService(); @@ -76,7 +76,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa // Fetch top 10 recently added items using abstraction layer 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) { return { success: true, message: 'No recent items', newCount: 0, updatedCount: 0, matchedDownloads: 0 }; @@ -112,7 +112,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa }, }); newCount++; - await logger?.info(`New item added: ${item.title} by ${item.author}`); + logger.info(`New item added: ${item.title} by ${item.author}`); } else { await prisma.plexLibrary.update({ where: { plexGuid: item.externalId }, @@ -144,7 +144,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa }); 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'); @@ -159,7 +159,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa }); 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 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 asin = audiobook.audibleAsin || undefined; 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'); await triggerABSItemMatch(itemId, asin); } } } 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 { success: true, @@ -209,7 +209,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa matchedDownloads, }; } 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; } } diff --git a/src/lib/processors/retry-failed-imports.processor.ts b/src/lib/processors/retry-failed-imports.processor.ts index 7b90029..ac1d41c 100644 --- a/src/lib/processors/retry-failed-imports.processor.ts +++ b/src/lib/processors/retry-failed-imports.processor.ts @@ -6,7 +6,7 @@ */ import { prisma } from '../db'; -import { createJobLogger } from '../utils/job-logger'; +import { RMABLogger } from '../utils/logger'; import { getJobQueueService } from '../services/job-queue.service'; import { getConfigService } from '../services/config.service'; import { PathMapper } from '../utils/path-mapper'; @@ -18,9 +18,9 @@ export interface RetryFailedImportsPayload { export async function processRetryFailedImports(payload: RetryFailedImportsPayload): Promise { 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 { // Load path mapping configuration once @@ -54,7 +54,7 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo 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) { return { @@ -75,7 +75,7 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo const downloadHistory = request.downloadHistory[0]; 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++; continue; } @@ -91,16 +91,16 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo const torrent = await qbt.getTorrent(downloadHistory.torrentHash); const qbPath = `${torrent.save_path}/${torrent.name}`; downloadPath = PathMapper.transform(qbPath, mappingConfig); - await logger?.info( + logger.info( `Got download path from qBittorrent for request ${request.id}: ${qbPath}` + (downloadPath !== qbPath ? ` → ${downloadPath} (mapped)` : '') ); } catch (qbtError) { // 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) { - 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++; continue; } @@ -108,14 +108,14 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo const downloadDir = await configService.get('download_dir'); 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++; continue; } const fallbackPath = `${downloadDir}/${downloadHistory.torrentName}`; downloadPath = PathMapper.transform(fallbackPath, mappingConfig); - await logger?.info( + logger.info( `Using fallback download path for request ${request.id}: ${fallbackPath}` + (downloadPath !== fallbackPath ? ` → ${downloadPath} (mapped)` : '') ); @@ -128,15 +128,15 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo const nzbInfo = await sabnzbd.getNZB(downloadHistory.nzbId); if (nzbInfo && nzbInfo.downloadPath) { downloadPath = PathMapper.transform(nzbInfo.downloadPath, mappingConfig); - await logger?.info( + logger.info( `Got download path from SABnzbd for request ${request.id}: ${nzbInfo.downloadPath}` + (downloadPath !== nzbInfo.downloadPath ? ` → ${downloadPath} (mapped)` : '') ); } 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) { - 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++; continue; } @@ -144,27 +144,27 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo const downloadDir = await configService.get('download_dir'); 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++; continue; } const fallbackPath = `${downloadDir}/${downloadHistory.torrentName}`; downloadPath = PathMapper.transform(fallbackPath, mappingConfig); - await logger?.info( + logger.info( `Using fallback download path for request ${request.id}: ${fallbackPath}` + (downloadPath !== fallbackPath ? ` → ${downloadPath} (mapped)` : '') ); } } 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++; continue; } } else { // No download client ID - use fallback path 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++; continue; } @@ -172,14 +172,14 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo const downloadDir = await configService.get('download_dir'); 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++; continue; } const configuredPath = `${downloadDir}/${downloadHistory.torrentName}`; downloadPath = PathMapper.transform(configuredPath, mappingConfig); - await logger?.info( + logger.info( `Using configured download path for request ${request.id}: ${configuredPath}` + (downloadPath !== configuredPath ? ` → ${downloadPath} (mapped)` : '') ); @@ -191,14 +191,14 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo downloadPath ); 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) { - 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++; } } - await logger?.info(`Triggered ${triggered}/${requests.length} organize jobs (${skipped} skipped)`); + logger.info(`Triggered ${triggered}/${requests.length} organize jobs (${skipped} skipped)`); return { success: true, @@ -208,7 +208,7 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo skipped, }; } 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; } } diff --git a/src/lib/processors/retry-missing-torrents.processor.ts b/src/lib/processors/retry-missing-torrents.processor.ts index 9922727..823fab9 100644 --- a/src/lib/processors/retry-missing-torrents.processor.ts +++ b/src/lib/processors/retry-missing-torrents.processor.ts @@ -6,7 +6,7 @@ */ import { prisma } from '../db'; -import { createJobLogger } from '../utils/job-logger'; +import { RMABLogger } from '../utils/logger'; import { getJobQueueService } from '../services/job-queue.service'; export interface RetryMissingTorrentsPayload { @@ -16,9 +16,9 @@ export interface RetryMissingTorrentsPayload { export async function processRetryMissingTorrents(payload: RetryMissingTorrentsPayload): Promise { 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 { // 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 }); - await logger?.info(`Found ${requests.length} requests awaiting search`); + logger.info(`Found ${requests.length} requests awaiting search`); if (requests.length === 0) { return { @@ -55,13 +55,13 @@ export async function processRetryMissingTorrents(payload: RetryMissingTorrentsP author: request.audiobook.author, }); 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) { - 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 { success: true, @@ -70,7 +70,7 @@ export async function processRetryMissingTorrents(payload: RetryMissingTorrentsP triggered, }; } 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; } } diff --git a/src/lib/processors/scan-plex.processor.ts b/src/lib/processors/scan-plex.processor.ts index 2e53cd3..87a328f 100644 --- a/src/lib/processors/scan-plex.processor.ts +++ b/src/lib/processors/scan-plex.processor.ts @@ -10,7 +10,7 @@ import { ScanPlexPayload } from '../services/job-queue.service'; import { prisma } from '../db'; import { getLibraryService } from '../services/library'; import { getConfigService } from '../services/config.service'; -import { createJobLogger } from '../utils/job-logger'; +import { RMABLogger } from '../utils/logger'; /** * Process library scan job @@ -19,9 +19,9 @@ import { createJobLogger } from '../utils/job-logger'; export async function processScanPlex(payload: ScanPlexPayload): Promise { 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 { // 1. Get library service (automatically selects Plex or Audiobookshelf based on config) @@ -29,7 +29,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { const configService = getConfigService(); const backendMode = await configService.getBackendMode(); - await logger?.info(`Backend mode: ${backendMode}`); + logger.info(`Backend mode: ${backendMode}`); // 2. Get configured library ID let targetLibraryId = libraryId; @@ -50,12 +50,12 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { } } - await logger?.info(`Fetching content from library ${targetLibraryId}`); + logger.info(`Fetching content from library ${targetLibraryId}`); // 3. Get all audiobooks from library using abstraction layer 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 updatedCount = 0; @@ -120,7 +120,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { }); newCount++; - await logger?.info(`Added new: "${item.title}" by ${item.author}`); + logger.info(`Added new: "${item.title}" by ${item.author}`); results.push({ id: newLibraryItem.id, @@ -130,16 +130,16 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { }); } } 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++; } } - 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) // 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 .filter(item => item.externalId) @@ -163,7 +163,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { }); 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 (const staleItem of staleLibraryItems) { @@ -214,7 +214,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { } } - 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 @@ -224,21 +224,21 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { staleRemovedCount++; } 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 { - await logger?.info(`No stale library records found`); + logger.info(`No stale library records found`); } } 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) // 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({ select: { plexGuid: true }, @@ -277,7 +277,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { // This audiobook is orphaned - its library link points to nothing 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 await prisma.audiobook.update({ @@ -306,18 +306,18 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { } } } 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) { - await logger?.info(`Reset ${orphanedAudiobooksReset} orphaned audiobooks and ${orphanedRequestsReset} requests`); + logger.info(`Reset ${orphanedAudiobooksReset} orphaned audiobooks and ${orphanedRequestsReset} requests`); } else { - await logger?.info(`No orphaned audiobooks found`); + logger.info(`No orphaned audiobooks found`); } // 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({ where: { status: 'downloaded', @@ -327,7 +327,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { 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; const { findPlexMatch } = await import('../utils/audiobook-matcher'); @@ -346,7 +346,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { }); 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) const updateData: any = { updatedAt: new Date() }; @@ -379,17 +379,17 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { const itemId = match.plexGuid; // plexGuid contains the Audiobookshelf item ID const asin = audiobook.audibleAsin || undefined; 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'); await triggerABSItemMatch(itemId, asin); } } } 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, newCount, updatedCount, @@ -420,7 +420,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise { matchedDownloads: matchedCount, }; } 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; } } diff --git a/src/lib/processors/search-indexers.processor.ts b/src/lib/processors/search-indexers.processor.ts index 1282c74..fed705c 100644 --- a/src/lib/processors/search-indexers.processor.ts +++ b/src/lib/processors/search-indexers.processor.ts @@ -7,7 +7,7 @@ import { SearchIndexersPayload, getJobQueueService } from '../services/job-queue import { prisma } from '../db'; import { getProwlarrService } from '../integrations/prowlarr.service'; import { getRankingAlgorithm } from '../utils/ranking-algorithm'; -import { createJobLogger } from '../utils/job-logger'; +import { RMABLogger } from '../utils/logger'; /** * Process search indexers job @@ -16,9 +16,9 @@ import { createJobLogger } from '../utils/job-logger'; export async function processSearchIndexers(payload: SearchIndexersPayload): Promise { 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 { // 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 flagConfigs = flagConfigStr ? JSON.parse(flagConfigStr) : []; - await logger?.info(`Searching ${enabledIndexerIds.length} enabled indexers`); + logger.info(`Searching ${enabledIndexerIds.length} enabled indexers`); // Get Prowlarr service 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) const searchQuery = audiobook.title; - await logger?.info(`Searching for: "${searchQuery}"`); + logger.info(`Searching for: "${searchQuery}"`); // Search indexers - ONLY enabled ones const searchResults = await prowlarr.search(searchQuery, { @@ -74,11 +74,11 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro 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) { // 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({ where: { id: requestId }, @@ -117,14 +117,14 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro result.score >= 50 && result.finalScore < 50 ).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) { - await logger?.info(`${disqualifiedByNegativeBonus} torrents disqualified by negative flag bonuses`); + logger.info(`${disqualifiedByNegativeBonus} torrents disqualified by negative flag bonuses`); } if (filteredResults.length === 0) { // 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({ where: { id: requestId }, @@ -148,38 +148,38 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro // Log top 3 results with detailed breakdown const top3 = filteredResults.slice(0, 3); - await logger?.info(`==================== RANKING DEBUG ====================`); - await logger?.info(`Requested Title: "${audiobook.title}"`); - await logger?.info(`Requested Author: "${audiobook.author}"`); - await logger?.info(`Top ${top3.length} results (out of ${filteredResults.length} above threshold):`); - await logger?.info(`--------------------------------------------------------`); + logger.info(`==================== RANKING DEBUG ====================`); + logger.info(`Requested Title: "${audiobook.title}"`); + logger.info(`Requested Author: "${audiobook.author}"`); + logger.info(`Top ${top3.length} results (out of ${filteredResults.length} above threshold):`); + logger.info(`--------------------------------------------------------`); for (let i = 0; i < top3.length; i++) { const result = top3[i]; - await logger?.info(`${i + 1}. "${result.title}"`); - await logger?.info(` Indexer: ${result.indexer}${result.indexerId ? ` (ID: ${result.indexerId})` : ''}`); - await logger?.info(``); - await logger?.info(` Base Score: ${result.score.toFixed(1)}/100`); - await 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'})`); - await logger?.info(` - Seeder Count: ${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders !== undefined ? result.seeders + ' seeders' : 'N/A for Usenet'})`); - await logger?.info(``); - await logger?.info(` Bonus Points: +${result.bonusPoints.toFixed(1)}`); + logger.info(`${i + 1}. "${result.title}"`); + logger.info(` Indexer: ${result.indexer}${result.indexerId ? ` (ID: ${result.indexerId})` : ''}`); + logger.info(``); + logger.info(` Base Score: ${result.score.toFixed(1)}/100`); + logger.info(` - Title/Author Match: ${result.breakdown.matchScore.toFixed(1)}/60`); + logger.info(` - Format Quality: ${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`); + logger.info(` - Seeder Count: ${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders !== undefined ? result.seeders + ' seeders' : 'N/A for Usenet'})`); + logger.info(``); + logger.info(` Bonus Points: +${result.bonusPoints.toFixed(1)}`); if (result.bonusModifiers.length > 0) { 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(``); - await logger?.info(` Final Score: ${result.finalScore.toFixed(1)}`); + logger.info(``); + logger.info(` Final Score: ${result.finalScore.toFixed(1)}`); 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) { - await logger?.info(`--------------------------------------------------------`); + logger.info(`--------------------------------------------------------`); } } - await logger?.info(`========================================================`); - await logger?.info(`Selected best result: ${bestResult.title} (final score: ${bestResult.finalScore.toFixed(1)})`); + logger.info(`========================================================`); + logger.info(`Selected best result: ${bestResult.title} (final score: ${bestResult.finalScore.toFixed(1)})`); // Trigger download job with best result const jobQueue = getJobQueueService(); @@ -202,7 +202,7 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro }, }; } 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({ where: { id: requestId }, diff --git a/src/lib/services/audiobookshelf/api.ts b/src/lib/services/audiobookshelf/api.ts index 3878e82..9ceffaf 100644 --- a/src/lib/services/audiobookshelf/api.ts +++ b/src/lib/services/audiobookshelf/api.ts @@ -4,6 +4,9 @@ */ import { getConfigService } from '../config.service'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('Audiobookshelf'); interface ABSRequestOptions { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; @@ -146,6 +149,6 @@ export async function triggerABSItemMatch(itemId: string, asin?: string) { }); } catch (error) { // 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) }); } } diff --git a/src/lib/services/auth/LocalAuthProvider.ts b/src/lib/services/auth/LocalAuthProvider.ts index 6e92f28..d158cf6 100644 --- a/src/lib/services/auth/LocalAuthProvider.ts +++ b/src/lib/services/auth/LocalAuthProvider.ts @@ -16,6 +16,9 @@ import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt'; import { getConfigService } from '@/lib/services/config.service'; import { getEncryptionService } from '@/lib/services/encryption.service'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('LocalAuth'); interface LocalLoginParams extends CallbackParams { username: string; @@ -83,7 +86,7 @@ export class LocalAuthProvider implements IAuthProvider { const decryptedHash = this.encryptionService.decrypt(user.authToken || ''); passwordValid = await bcrypt.compare(password, decryptedHash); } catch (error) { - console.error('[LocalAuthProvider] Password verification failed:', error); + logger.error('Password verification failed', { error: error instanceof Error ? error.message : String(error) }); return { success: false, error: 'Invalid username or password' }; } @@ -98,7 +101,7 @@ export class LocalAuthProvider implements IAuthProvider { }); // Generate tokens - console.log('[LocalAuthProvider] Generating tokens for user:', { + logger.info('Generating tokens for user', { id: user.id, plexId: user.plexId, username: user.plexUsername, @@ -113,7 +116,7 @@ export class LocalAuthProvider implements IAuthProvider { isAdmin: user.role === 'admin', }); - console.log('[LocalAuthProvider] Tokens generated, returning user data'); + logger.info('Tokens generated, returning user data'); return { success: true, @@ -126,7 +129,7 @@ export class LocalAuthProvider implements IAuthProvider { tokens, }; } catch (error) { - console.error('[LocalAuthProvider] Login failed:', error); + logger.error('Login failed', { error: error instanceof Error ? error.message : String(error) }); return { success: false, error: error instanceof Error ? error.message : 'Authentication failed', @@ -224,7 +227,7 @@ export class LocalAuthProvider implements IAuthProvider { tokens, }; } catch (error) { - console.error('[LocalAuthProvider] Registration failed:', error); + logger.error('Registration failed', { error: error instanceof Error ? error.message : String(error) }); return { success: false, error: error instanceof Error ? error.message : 'Registration failed', @@ -243,7 +246,7 @@ export class LocalAuthProvider implements IAuthProvider { role: userInfo.isAdmin ? 'admin' : 'user', }; - console.log('[LocalAuthProvider] JWT token payload:', tokenPayload); + logger.debug('JWT token payload', { tokenPayload }); const accessToken = generateAccessToken(tokenPayload); const refreshToken = generateRefreshToken(userInfo.id); @@ -288,7 +291,7 @@ export class LocalAuthProvider implements IAuthProvider { return true; } catch (error) { - console.error('[LocalAuthProvider] Access validation failed:', error); + logger.error('Access validation failed', { error: error instanceof Error ? error.message : String(error) }); return false; } } diff --git a/src/lib/services/auth/OIDCAuthProvider.ts b/src/lib/services/auth/OIDCAuthProvider.ts index 66165ab..7a768bc 100644 --- a/src/lib/services/auth/OIDCAuthProvider.ts +++ b/src/lib/services/auth/OIDCAuthProvider.ts @@ -18,6 +18,9 @@ import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt'; import { getBaseUrl } from '@/lib/utils/url'; import { getSchedulerService } from '@/lib/services/scheduler.service'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('OIDCAuth'); // In-memory storage for OIDC flow state (temporary until callback completes) // In production, this could be replaced with Redis for multi-instance support @@ -109,7 +112,7 @@ export class OIDCAuthProvider implements IAuthProvider { state, }; } catch (error) { - console.error('[OIDCAuthProvider] Failed to initiate login:', error); + logger.error('Failed to initiate login', { error: error instanceof Error ? error.message : String(error) }); throw new Error('Failed to initiate OIDC authentication'); } } @@ -150,14 +153,12 @@ export class OIDCAuthProvider implements IAuthProvider { const client = await this.getClient(); const redirectUri = await this.getRedirectUri(); - if (process.env.LOG_LEVEL === 'debug') { - console.debug('[OIDCAuthProvider] Exchanging code for tokens', { - redirectUri, - hasCode: !!code, - hasState: !!state, - stateMatches: state === flowState.state, - }); - } + logger.debug('Exchanging code for tokens', { + redirectUri, + hasCode: !!code, + hasState: !!state, + stateMatches: state === flowState.state, + }); // Exchange code for tokens const tokenSet = await client.callback( @@ -259,7 +260,7 @@ export class OIDCAuthProvider implements IAuthProvider { isFirstLogin: result.isFirstLogin, }; } catch (error) { - console.error('[OIDCAuthProvider] Callback failed:', error); + logger.error('Callback failed', { error: error instanceof Error ? error.message : String(error) }); return { success: false, error: error instanceof Error ? error.message : 'Authentication failed', @@ -282,7 +283,7 @@ export class OIDCAuthProvider implements IAuthProvider { const requiredGroup = await this.configService.get('oidc.access_group_value'); if (!requiredGroup) { - console.error('[OIDCAuthProvider] Group claim access control enabled but no required group configured'); + logger.error('Group claim access control enabled but no required group configured'); return false; } @@ -432,7 +433,7 @@ export class OIDCAuthProvider implements IAuthProvider { // If this is the first user, trigger initial jobs (Audible refresh + Library scan) // This happens after OIDC-only setup where no admin was created during wizard if (isFirstUser) { - console.log('[OIDCAuthProvider] First OIDC user created - triggering initial jobs'); + logger.info('First OIDC user created - triggering initial jobs'); // Check if initial jobs have already been run (avoid duplicate runs) const initialJobsRun = await this.configService.get('system.initial_jobs_run'); @@ -442,7 +443,7 @@ export class OIDCAuthProvider implements IAuthProvider { // Trigger jobs in background (don't block authentication) this.triggerInitialJobs().catch(err => { - console.error('[OIDCAuthProvider] Failed to trigger initial jobs:', err); + logger.error('Failed to trigger initial jobs', { error: err instanceof Error ? err.message : String(err) }); }); } } @@ -476,22 +477,22 @@ export class OIDCAuthProvider implements IAuthProvider { where: { type: 'plex_library_scan' }, }); - console.log('[OIDCAuthProvider] Triggering initial jobs...'); + logger.info('Triggering initial jobs...'); // Trigger Audible refresh if (audibleJob) { await schedulerService.triggerJobNow(audibleJob.id); - console.log('[OIDCAuthProvider] Triggered Audible refresh job'); + logger.info('Triggered Audible refresh job'); } else { - console.warn('[OIDCAuthProvider] Audible refresh job not found'); + logger.warn('Audible refresh job not found'); } // Trigger Library scan if (libraryJob) { await schedulerService.triggerJobNow(libraryJob.id); - console.log('[OIDCAuthProvider] Triggered Library scan job'); + logger.info('Triggered Library scan job'); } else { - console.warn('[OIDCAuthProvider] Library scan job not found'); + logger.warn('Library scan job not found'); } // Mark initial jobs as run @@ -501,9 +502,9 @@ export class OIDCAuthProvider implements IAuthProvider { create: { key: 'system.initial_jobs_run', value: 'true' }, }); - console.log('[OIDCAuthProvider] Initial jobs triggered successfully'); + logger.info('Initial jobs triggered successfully'); } catch (error) { - console.error('[OIDCAuthProvider] Error triggering initial jobs:', error); + logger.error('Error triggering initial jobs', { error: error instanceof Error ? error.message : String(error) }); throw error; } } @@ -556,7 +557,7 @@ export class OIDCAuthProvider implements IAuthProvider { return true; } catch (error) { - console.error('[OIDCAuthProvider] Access validation failed:', error); + logger.error('Access validation failed', { error: error instanceof Error ? error.message : String(error) }); return false; } } diff --git a/src/lib/services/auth/PlexAuthProvider.ts b/src/lib/services/auth/PlexAuthProvider.ts index 7bd4337..7fb6731 100644 --- a/src/lib/services/auth/PlexAuthProvider.ts +++ b/src/lib/services/auth/PlexAuthProvider.ts @@ -17,6 +17,9 @@ import { getEncryptionService } from '@/lib/services/encryption.service'; import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt'; import { getBaseUrl } from '@/lib/utils/url'; import { prisma } from '@/lib/db'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('PlexAuth'); export class PlexAuthProvider implements IAuthProvider { type: 'plex' = 'plex'; @@ -43,7 +46,7 @@ export class PlexAuthProvider implements IAuthProvider { pinId: pin.id.toString(), }; } catch (error) { - console.error('[PlexAuthProvider] Failed to initiate login:', error); + logger.error('Failed to initiate login', { error: error instanceof Error ? error.message : String(error) }); throw new Error('Failed to initiate Plex authentication'); } } @@ -137,7 +140,7 @@ export class PlexAuthProvider implements IAuthProvider { tokens, }; } catch (error) { - console.error('[PlexAuthProvider] Callback failed:', error); + logger.error('Callback failed', { error: error instanceof Error ? error.message : String(error) }); return { success: false, error: error instanceof Error ? error.message : 'Authentication failed', @@ -184,7 +187,7 @@ export class PlexAuthProvider implements IAuthProvider { decryptedToken ); } catch (error) { - console.error('[PlexAuthProvider] Access validation failed:', error); + logger.error('Access validation failed', { error: error instanceof Error ? error.message : String(error) }); return false; } } diff --git a/src/lib/services/config.service.ts b/src/lib/services/config.service.ts index 59d8c36..2070a50 100644 --- a/src/lib/services/config.service.ts +++ b/src/lib/services/config.service.ts @@ -5,6 +5,9 @@ import { prisma } from '@/lib/db'; import { getEncryptionService } from './encryption.service'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('Config'); /** * Configuration update payload @@ -70,7 +73,7 @@ export class ConfigurationService { return null; } catch (error) { - console.error(`[Config] Failed to get config key "${key}":`, error); + logger.error(`Failed to get config key "${key}"`, { error: error instanceof Error ? error.message : String(error) }); return null; } } @@ -119,7 +122,7 @@ export class ConfigurationService { return result; } catch (error) { - console.error(`[Config] Failed to get category "${category}":`, error); + logger.error(`Failed to get category "${category}"`, { error: error instanceof Error ? error.message : String(error) }); return {}; } } @@ -144,7 +147,7 @@ export class ConfigurationService { return result; } catch (error) { - console.error('[Config] Failed to get all configuration:', error); + logger.error('Failed to get all configuration', { error: error instanceof Error ? error.message : String(error) }); return {}; } } @@ -186,7 +189,7 @@ export class ConfigurationService { this.clearCache(update.key); } } catch (error) { - console.error('[Config] Failed to set configuration:', error); + logger.error('Failed to set configuration', { error: error instanceof Error ? error.message : String(error) }); throw error; } } diff --git a/src/lib/services/ebook-scraper.ts b/src/lib/services/ebook-scraper.ts index 8604810..4cf8b2c 100644 --- a/src/lib/services/ebook-scraper.ts +++ b/src/lib/services/ebook-scraper.ts @@ -8,6 +8,10 @@ import * as cheerio from 'cheerio'; import fs from 'fs/promises'; import path from 'path'; import { JobLogger } from '../utils/job-logger'; +import { RMABLogger } from '../utils/logger'; + +// Module-level logger (renamed to avoid shadowing function parameter 'logger') +const moduleLogger = RMABLogger.create('EbookScraper'); export interface EbookDownloadResult { success: boolean; @@ -23,9 +27,6 @@ const MAX_SLOW_LINK_ATTEMPTS = 5; const MAX_RETRIES = 3; const FLARESOLVERR_TIMEOUT_MS = 60000; // 60 seconds for FlareSolverr requests -// Debug logging -const DEBUG_ENABLED = process.env.LOG_LEVEL === 'debug'; - // In-memory cache for MD5 lookups (prevents re-scraping same ASIN) const md5Cache = new Map(); @@ -94,13 +95,9 @@ async function fetchHtml( // Try FlareSolverr first if configured if (flaresolverrUrl) { try { - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] Using FlareSolverr for: ${url}`); - } + moduleLogger.debug(`Using FlareSolverr for: ${url}`); const html = await fetchViaFlareSolverr(url, flaresolverrUrl); - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] FlareSolverr returned HTML length: ${html.length}`); - } + moduleLogger.debug(`FlareSolverr returned HTML length: ${html.length}`); return html; } catch (error) { await logger?.warn( @@ -108,17 +105,13 @@ async function fetchHtml( error instanceof Error ? error.message : 'Unknown error' }` ); - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] FlareSolverr error:`, error); - } + moduleLogger.debug('FlareSolverr error', { error: error instanceof Error ? error.message : String(error) }); // Fall through to direct request } } // Direct request (may fail with Cloudflare protection) - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] Using direct request for: ${url}`); - } + moduleLogger.debug(`Using direct request for: ${url}`); const response = await retryRequest(() => axios.get(url, { headers: { 'User-Agent': USER_AGENT }, @@ -126,9 +119,7 @@ async function fetchHtml( }) ); - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] Direct request returned data length: ${response.data?.length || 0}`); - } + moduleLogger.debug(`Direct request returned data length: ${response.data?.length || 0}`); return response.data; } @@ -337,9 +328,7 @@ async function searchByAsin( const formatParam = format && format !== 'any' ? `ext=${format}&` : ''; const searchUrl = `${baseUrl}/search?${formatParam}q=%22asin:${asin}%22`; - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] ASIN search URL: ${searchUrl}`); - } + moduleLogger.debug(`ASIN search URL: ${searchUrl}`); const html = await fetchHtml(searchUrl, flaresolverrUrl, logger); const $ = cheerio.load(html); @@ -358,26 +347,24 @@ async function searchByAsin( return true; }); - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] ASIN search HTML length: ${html.length}`); - // Log the page title to see what we got - const pageTitle = $('title').text(); - console.log(`[EbookScraper] ASIN search page title: ${pageTitle}`); - // Count how many md5 links we found (excluding recent downloads) - const allMd5Links = $('a[href*="/md5/"]').length; - console.log(`[EbookScraper] Total MD5 links on page: ${allMd5Links}, search results only: ${searchResultLinks.length}`); - } + // Debug logging for ASIN search + const pageTitle = $('title').text(); + const allMd5Links = $('a[href*="/md5/"]').length; + moduleLogger.debug('ASIN search results', { + htmlLength: html.length, + pageTitle, + totalMd5Links: allMd5Links, + searchResultLinks: searchResultLinks.length + }); // Extract MD5 from first search result link const firstResult = searchResultLinks.first(); const href = firstResult.attr('href'); - if (DEBUG_ENABLED && firstResult.length > 0) { - // Try to get the text/title of the first result + if (firstResult.length > 0) { const resultText = firstResult.text().trim().substring(0, 100); const parentText = firstResult.parent().text().trim().substring(0, 100); - console.log(`[EbookScraper] First result link text: "${resultText}"`); - console.log(`[EbookScraper] First result parent text: "${parentText}"`); + moduleLogger.debug('First result details', { resultText, parentText }); } if (!href) { @@ -390,9 +377,7 @@ async function searchByAsin( const md5Match = href.match(/\/md5\/([a-f0-9]+)/); const md5 = md5Match ? md5Match[1] : null; - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] Extracted MD5 from ASIN search: ${md5}`); - } + moduleLogger.debug(`Extracted MD5 from ASIN search: ${md5}`); // Cache result md5Cache.set(cacheKey, md5); @@ -451,9 +436,7 @@ async function searchByTitle( // Empty raw query (we're using specific terms instead) searchUrl += '&q='; - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] Title search URL: ${searchUrl}`); - } + moduleLogger.debug(`Title search URL: ${searchUrl}`); const html = await fetchHtml(searchUrl, flaresolverrUrl, logger); const $ = cheerio.load(html); @@ -471,10 +454,8 @@ async function searchByTitle( return true; }); - if (DEBUG_ENABLED) { - const allMd5Links = $('a[href*="/md5/"]').length; - console.log(`[EbookScraper] Title search: Total MD5 links: ${allMd5Links}, search results only: ${searchResultLinks.length}`); - } + const allMd5Links = $('a[href*="/md5/"]').length; + moduleLogger.debug('Title search results', { totalMd5Links: allMd5Links, searchResultLinks: searchResultLinks.length }); // Extract MD5 from first search result link const firstResult = searchResultLinks.first(); @@ -516,44 +497,35 @@ async function getSlowDownloadLinks( try { const md5Url = `${baseUrl}/md5/${md5}`; - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] Fetching MD5 page: ${md5Url}`); - } + moduleLogger.debug(`Fetching MD5 page: ${md5Url}`); const html = await fetchHtml(md5Url, flaresolverrUrl, logger); - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] HTML length: ${html.length}`); - console.log(`[EbookScraper] HTML preview (first 500 chars): ${html.substring(0, 500)}`); - // Check if we got a Cloudflare challenge page - if (html.includes('challenge-running') || html.includes('cf-browser-verification')) { - console.log(`[EbookScraper] WARNING: Appears to be Cloudflare challenge page!`); - } + moduleLogger.debug('MD5 page HTML', { length: html.length, preview: html.substring(0, 500) }); + // Check if we got a Cloudflare challenge page + if (html.includes('challenge-running') || html.includes('cf-browser-verification')) { + moduleLogger.warn('Appears to be Cloudflare challenge page'); } const $ = cheerio.load(html); const slowLinks: string[] = []; // Debug: count all links - if (DEBUG_ENABLED) { - const allLinks = $('a').length; - const slowDownloadLinks = $('a[href*="/slow_download/"]').length; - const slowDownloadLinksAlt = $('a[href*="slow_download"]').length; - console.log(`[EbookScraper] Total links on page: ${allLinks}`); - console.log(`[EbookScraper] Links with /slow_download/: ${slowDownloadLinks}`); - console.log(`[EbookScraper] Links with slow_download (no slashes): ${slowDownloadLinksAlt}`); + const allLinks = $('a').length; + const slowDownloadLinks = $('a[href*="/slow_download/"]').length; + const slowDownloadLinksAlt = $('a[href*="slow_download"]').length; + moduleLogger.debug('Link counts on page', { allLinks, slowDownloadLinks, slowDownloadLinksAlt }); - // Log all href patterns to see what we're dealing with - const hrefPatterns: string[] = []; - $('a[href]').each((i, elem) => { - const href = $(elem).attr('href') || ''; - if (href.includes('download') || href.includes('slow')) { - hrefPatterns.push(href.substring(0, 100)); - } - }); - if (hrefPatterns.length > 0) { - console.log(`[EbookScraper] Download-related hrefs found:`, hrefPatterns.slice(0, 10)); + // Log all href patterns to see what we're dealing with + const hrefPatterns: string[] = []; + $('a[href]').each((i, elem) => { + const href = $(elem).attr('href') || ''; + if (href.includes('download') || href.includes('slow')) { + hrefPatterns.push(href.substring(0, 100)); } + }); + if (hrefPatterns.length > 0) { + moduleLogger.debug('Download-related hrefs found', { hrefs: hrefPatterns.slice(0, 10) }); } // Find all slow download links @@ -563,28 +535,21 @@ async function getSlowDownloadLinks( // e.g.,
  • Slow Partner Server #5 (no waitlist, but can be very slow)
  • const parentText = $(elem).parent().text().toLowerCase(); - if (DEBUG_ENABLED) { - const href = $(elem).attr('href'); - console.log(`[EbookScraper] Found slow_download link: href="${href}", linkText="${linkText.substring(0, 30)}", parentText="${parentText.substring(0, 60)}"`); - } + const href = $(elem).attr('href'); + moduleLogger.debug('Found slow_download link', { href, linkText: linkText.substring(0, 30), parentText: parentText.substring(0, 60) }); // Check for "no waitlist" in either the link text or parent text if (linkText.includes('no waitlist') || parentText.includes('no waitlist')) { - const href = $(elem).attr('href'); if (href) { // Convert relative URL to absolute const fullUrl = href.startsWith('http') ? href : `${baseUrl}${href}`; slowLinks.push(fullUrl); - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] Added slow link (no waitlist): ${fullUrl}`); - } + moduleLogger.debug(`Added slow link (no waitlist): ${fullUrl}`); } } }); - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] Total slow links found: ${slowLinks.length}`); - } + moduleLogger.debug(`Total slow links found: ${slowLinks.length}`); await delay(REQUEST_DELAY_MS); return slowLinks; @@ -592,9 +557,7 @@ async function getSlowDownloadLinks( await logger?.error( `Failed to get slow links: ${error instanceof Error ? error.message : 'Unknown error'}` ); - if (DEBUG_ENABLED) { - console.log(`[EbookScraper] Error getting slow links:`, error); - } + moduleLogger.debug('Error getting slow links', { error: error instanceof Error ? error.message : String(error) }); return []; } } diff --git a/src/lib/services/job-queue.service.ts b/src/lib/services/job-queue.service.ts index de6892d..f10cefa 100644 --- a/src/lib/services/job-queue.service.ts +++ b/src/lib/services/job-queue.service.ts @@ -7,6 +7,9 @@ import Queue, { Job as BullJob, JobOptions } from 'bull'; import Redis from 'ioredis'; import { prisma } from '../db'; import { TorrentResult } from '../utils/ranking-algorithm'; +import { RMABLogger } from '../utils/logger'; + +const logger = RMABLogger.create('JobQueue'); export type JobType = | 'search_indexers' @@ -151,12 +154,12 @@ export class JobQueueService { */ private setupEventHandlers(): void { this.queue.on('completed', async (job: BullJob, result: any) => { - console.log(`Job ${job.id} completed:`, result); + logger.info(`Job ${job.id} completed`, { result }); await this.updateJobInDatabase(job.id as string, 'completed', result); }); this.queue.on('failed', async (job: BullJob, error: Error) => { - console.error(`Job ${job.id} failed:`, error.message); + logger.error(`Job ${job.id} failed`, { error: error.message }); await this.updateJobInDatabase( job.id as string, 'failed', @@ -168,7 +171,7 @@ export class JobQueueService { // Handle permanent failures for specific job types after all retries exhausted if (job.name === 'monitor_download' && job.data) { const payload = job.data as MonitorDownloadPayload; - console.error(`[MonitorDownload] Job permanently failed for request ${payload.requestId} after ${job.attemptsMade} attempts`); + logger.error(`MonitorDownload job permanently failed for request ${payload.requestId} after ${job.attemptsMade} attempts`); // Update request status to failed (only happens after all retries exhausted) try { @@ -192,13 +195,13 @@ export class JobQueueService { }); } } catch (updateError) { - console.error('[MonitorDownload] Failed to update request/download status:', updateError); + logger.error('Failed to update request/download status', { error: updateError instanceof Error ? updateError.message : String(updateError) }); } } }); this.queue.on('stalled', async (job: BullJob) => { - console.warn(`Job ${job.id} stalled`); + logger.warn(`Job ${job.id} stalled`); await this.updateJobInDatabase(job.id as string, 'stuck'); }); @@ -207,7 +210,7 @@ export class JobQueueService { }); this.queue.on('error', (error: Error) => { - console.error('Queue error:', error); + logger.error('Queue error', { error: error.message }); }); } @@ -322,7 +325,7 @@ export class JobQueueService { where: { id: payload.scheduledJobId }, data: { lastRun: new Date() }, }).catch(err => { - console.error(`[JobQueue] Failed to update lastRun for scheduled job ${payload.scheduledJobId}:`, err); + logger.error(`Failed to update lastRun for scheduled job ${payload.scheduledJobId}`, { error: err instanceof Error ? err.message : String(err) }); }); } return { ...payload, jobId: existingJob.id }; @@ -347,7 +350,7 @@ export class JobQueueService { where: { id: payload.scheduledJobId }, data: { lastRun: new Date() }, }).catch(err => { - console.error(`[JobQueue] Failed to update lastRun for scheduled job ${payload.scheduledJobId}:`, err); + logger.error(`Failed to update lastRun for scheduled job ${payload.scheduledJobId}`, { error: err instanceof Error ? err.message : String(err) }); }); } @@ -395,7 +398,7 @@ export class JobQueueService { data: updateData, }); } catch (error) { - console.error('Failed to update job in database:', error); + logger.error('Failed to update job in database', { error: error instanceof Error ? error.message : String(error) }); } } @@ -801,7 +804,7 @@ export class JobQueueService { }, jobId, }); - console.log(`[JobQueue] Added repeatable job: ${jobType} with cron ${cronExpression}`); + logger.info(`Added repeatable job: ${jobType} with cron ${cronExpression}`); } /** @@ -816,7 +819,7 @@ export class JobQueueService { cron: cronExpression, jobId, }); - console.log(`[JobQueue] Removed repeatable job: ${jobType}`); + logger.info(`Removed repeatable job: ${jobType}`); } /** @@ -840,7 +843,7 @@ export function getJobQueueService(): JobQueueService { // Graceful shutdown process.on('SIGTERM', async () => { if (jobQueueService) { - console.log('Closing job queue...'); + logger.info('Closing job queue...'); await jobQueueService.close(); } }); diff --git a/src/lib/services/library/PlexLibraryService.ts b/src/lib/services/library/PlexLibraryService.ts index 9621660..4fd57c4 100644 --- a/src/lib/services/library/PlexLibraryService.ts +++ b/src/lib/services/library/PlexLibraryService.ts @@ -12,6 +12,9 @@ import { } from './ILibraryService'; import { getPlexService } from '@/lib/integrations/plex.service'; import { getConfigService } from '@/lib/services/config.service'; +import { RMABLogger } from '@/lib/utils/logger'; + +const logger = RMABLogger.create('PlexLibrary'); export class PlexLibraryService implements ILibraryService { private plexService = getPlexService(); @@ -175,7 +178,7 @@ export class PlexLibraryService implements ILibraryService { // This is a simplified implementation return null; } catch (error) { - console.error('[PlexLibraryService] Failed to get item:', error); + logger.error('Failed to get item', { error: error instanceof Error ? error.message : String(error) }); return null; } } diff --git a/src/lib/services/request-delete.service.ts b/src/lib/services/request-delete.service.ts index 4d08f74..dbcc0d5 100644 --- a/src/lib/services/request-delete.service.ts +++ b/src/lib/services/request-delete.service.ts @@ -8,6 +8,9 @@ import { prisma } from '../db'; import * as fs from 'fs/promises'; import * as path from 'path'; +import { RMABLogger } from '../utils/logger'; + +const logger = RMABLogger.create('RequestDelete'); export interface DeleteRequestResult { success: boolean; @@ -111,7 +114,7 @@ export async function deleteRequest( torrent = await qbt.getTorrent(downloadHistory.torrentHash); } catch (error) { // Torrent not found in qBittorrent (already removed) - console.log(`[RequestDelete] Torrent ${downloadHistory.torrentHash} not found in qBittorrent, skipping`); + logger.info(`Torrent ${downloadHistory.torrentHash} not found in qBittorrent, skipping`); } if (torrent) { @@ -121,14 +124,14 @@ export async function deleteRequest( if (isUnlimitedSeeding) { // Unlimited seeding - keep in qBittorrent, stop monitoring - console.log( - `[RequestDelete] Keeping torrent ${torrent.name} for unlimited seeding (indexer: ${downloadHistory.indexerName})` + logger.info( + `Keeping torrent ${torrent.name} for unlimited seeding (indexer: ${downloadHistory.indexerName})` ); torrentsKeptUnlimited++; } else if (!isCompleted) { // Download not completed - delete immediately - console.log( - `[RequestDelete] Deleting incomplete download: ${torrent.name}` + logger.info( + `Deleting incomplete download: ${torrent.name}` ); await qbt.deleteTorrent(downloadHistory.torrentHash, true); torrentsRemoved++; @@ -140,8 +143,8 @@ export async function deleteRequest( if (hasMetRequirement) { // Seeding requirement met - delete now - console.log( - `[RequestDelete] Deleting torrent ${torrent.name} (seeding complete: ${Math.floor( + logger.info( + `Deleting torrent ${torrent.name} (seeding complete: ${Math.floor( actualSeedingTime / 60 )}/${seedingConfig.seedingTimeMinutes} minutes)` ); @@ -150,8 +153,8 @@ export async function deleteRequest( } else { // Still needs seeding - keep for cleanup job const remainingMinutes = Math.ceil((seedingTimeSeconds - actualSeedingTime) / 60); - console.log( - `[RequestDelete] Keeping torrent ${torrent.name} for ${remainingMinutes} more minutes of seeding` + logger.info( + `Keeping torrent ${torrent.name} for ${remainingMinutes} more minutes of seeding` ); torrentsKeptSeeding++; } @@ -165,17 +168,17 @@ export async function deleteRequest( // Try to delete the NZB from SABnzbd (might already be completed/removed) await sabnzbd.deleteNZB(downloadHistory.nzbId, true); - console.log(`[RequestDelete] Deleted NZB ${downloadHistory.nzbId} from SABnzbd`); + logger.info(`Deleted NZB ${downloadHistory.nzbId} from SABnzbd`); torrentsRemoved++; } catch (error) { // NZB not found or already removed - console.log(`[RequestDelete] NZB ${downloadHistory.nzbId} not found in SABnzbd, skipping`); + logger.info(`NZB ${downloadHistory.nzbId} not found in SABnzbd, skipping`); } } } catch (error) { - console.error( - `[RequestDelete] Error handling download for request ${requestId}:`, - error instanceof Error ? error.message : 'Unknown error' + logger.error( + `Error handling download for request ${requestId}`, + { error: error instanceof Error ? error.message : String(error) } ); // Continue with deletion even if download handling fails } @@ -229,7 +232,7 @@ export async function deleteRequest( // Delete the title folder (not the author folder) await fs.rm(titleFolderPath, { recursive: true, force: true }); - console.log(`[RequestDelete] Deleted media directory: ${titleFolderPath}`); + logger.info(`Deleted media directory: ${titleFolderPath}`); filesDeleted = true; } catch (accessError) { // Folder doesn't exist - try without year/ASIN (fallback for older files) @@ -237,20 +240,20 @@ export async function deleteRequest( try { await fs.access(fallbackPath); await fs.rm(fallbackPath, { recursive: true, force: true }); - console.log(`[RequestDelete] Deleted media directory (fallback path): ${fallbackPath}`); + logger.info(`Deleted media directory (fallback path): ${fallbackPath}`); filesDeleted = true; } catch (fallbackError) { // Neither path exists - that's okay - console.log( - `[RequestDelete] Media directory not found (tried: ${titleFolderPath}, ${fallbackPath})` + logger.info( + `Media directory not found (tried: ${titleFolderPath}, ${fallbackPath})` ); filesDeleted = false; } } } catch (error) { - console.error( - `[RequestDelete] Error deleting media files for request ${requestId}:`, - error instanceof Error ? error.message : 'Unknown error' + logger.error( + `Error deleting media files for request ${requestId}`, + { error: error instanceof Error ? error.message : String(error) } ); // Continue with soft delete even if file deletion fails } @@ -291,18 +294,18 @@ export async function deleteRequest( await Promise.all(deletePromises); - console.log( - `[RequestDelete] Deleted ${exactMatches.length} plex_library record(s) for "${request.audiobook.title}"` + logger.info( + `Deleted ${exactMatches.length} plex_library record(s) for "${request.audiobook.title}"` ); } else { - console.log( - `[RequestDelete] No plex_library records found for "${request.audiobook.title}"` + logger.info( + `No plex_library records found for "${request.audiobook.title}"` ); } } catch (libError) { - console.error( - `[RequestDelete] Error deleting plex_library records:`, - libError instanceof Error ? libError.message : 'Unknown error' + logger.error( + `Error deleting plex_library records`, + { error: libError instanceof Error ? libError.message : String(libError) } ); // Continue with deletion even if library cleanup fails } @@ -325,13 +328,13 @@ export async function deleteRequest( data: updateData, }); - console.log( - `[RequestDelete] Cleared availability status for audiobook ${request.audiobook.id}` + logger.info( + `Cleared availability status for audiobook ${request.audiobook.id}` ); } catch (error) { - console.error( - `[RequestDelete] Error clearing audiobook status:`, - error instanceof Error ? error.message : 'Unknown error' + logger.error( + `Error clearing audiobook status`, + { error: error instanceof Error ? error.message : String(error) } ); // Continue with deletion even if this fails } @@ -345,8 +348,8 @@ export async function deleteRequest( }, }); - console.log( - `[RequestDelete] Request ${requestId} soft-deleted by admin ${adminUserId}` + logger.info( + `Request ${requestId} soft-deleted by admin ${adminUserId}` ); return { @@ -358,9 +361,9 @@ export async function deleteRequest( torrentsKeptUnlimited, }; } catch (error) { - console.error( - `[RequestDelete] Failed to delete request ${requestId}:`, - error instanceof Error ? error.message : 'Unknown error' + logger.error( + `Failed to delete request ${requestId}`, + { error: error instanceof Error ? error.message : String(error) } ); return { diff --git a/src/lib/services/scheduler.service.ts b/src/lib/services/scheduler.service.ts index 28a2be3..541ee5c 100644 --- a/src/lib/services/scheduler.service.ts +++ b/src/lib/services/scheduler.service.ts @@ -5,6 +5,9 @@ import { getJobQueueService, ScanPlexPayload } from './job-queue.service'; import { prisma } from '../db'; +import { RMABLogger } from '../utils/logger'; + +const logger = RMABLogger.create('Scheduler'); export type ScheduledJobType = 'plex_library_scan' | 'plex_recently_added_check' | 'audible_refresh' | 'retry_missing_torrents' | 'retry_failed_imports' | 'cleanup_seeded_torrents' | 'monitor_rss_feeds'; @@ -44,7 +47,7 @@ export class SchedulerService { * Initialize scheduler and set up default jobs if they don't exist */ async start(): Promise { - console.log('[Scheduler] Initializing scheduler service...'); + logger.info('Initializing scheduler service...'); // Create default jobs if they don't exist await this.ensureDefaultJobs(); @@ -55,7 +58,7 @@ export class SchedulerService { // Check and trigger overdue jobs await this.triggerOverdueJobs(); - console.log('[Scheduler] Scheduler service started'); + logger.info('Scheduler service started'); } /** @@ -123,7 +126,7 @@ export class SchedulerService { await prisma.scheduledJob.create({ data: defaultJob, }); - console.log(`[Scheduler] Created default job: ${defaultJob.name} (disabled by default)`); + logger.info(`Created default job: ${defaultJob.name} (disabled by default)`); } } } @@ -140,7 +143,7 @@ export class SchedulerService { await this.scheduleJob(job); } - console.log(`[Scheduler] Scheduled ${jobs.length} jobs`); + logger.info(`Scheduled ${jobs.length} jobs`); } /** @@ -154,9 +157,9 @@ export class SchedulerService { job.schedule, `scheduled-${job.id}` ); - console.log(`[Scheduler] Job scheduled: ${job.name} (${job.schedule})`); + logger.info(`Job scheduled: ${job.name} (${job.schedule})`); } catch (error) { - console.error(`[Scheduler] Failed to schedule job ${job.name}:`, error); + logger.error(`Failed to schedule job ${job.name}`, { error: error instanceof Error ? error.message : String(error) }); throw error; } } @@ -171,9 +174,9 @@ export class SchedulerService { job.schedule, `scheduled-${job.id}` ); - console.log(`[Scheduler] Job unscheduled: ${job.name}`); + logger.info(`Job unscheduled: ${job.name}`); } catch (error) { - console.error(`[Scheduler] Failed to unschedule job ${job.name}:`, error); + logger.error(`Failed to unschedule job ${job.name}`, { error: error instanceof Error ? error.message : String(error) }); // Don't throw - job might not exist in Bull yet } } @@ -324,7 +327,7 @@ export class SchedulerService { }, }); - console.log(`[Scheduler] Job "${job.name}" triggered with Bull job ID: ${bullJobId}`); + logger.info(`Job "${job.name}" triggered with Bull job ID: ${bullJobId}`); return bullJobId; } @@ -362,7 +365,7 @@ export class SchedulerService { if (missingFields.length > 0) { const errorMsg = `Audiobookshelf is not configured. Missing: ${missingFields.join(', ')}. Please configure Audiobookshelf in the admin settings before running library scans.`; - console.error('[ScanLibrary] Error:', errorMsg); + logger.error(errorMsg); throw new Error(errorMsg); } @@ -386,14 +389,14 @@ export class SchedulerService { if (missingFields.length > 0) { const errorMsg = `Plex is not configured. Missing: ${missingFields.join(', ')}. Please configure Plex in the admin settings before running library scans.`; - console.error('[ScanLibrary] Error:', errorMsg); + logger.error(errorMsg); throw new Error(errorMsg); } libraryId = job.payload?.libraryId || plexConfig.plex_audiobook_library_id; } - console.log(`[ScanLibrary] Triggering ${backendMode} library scan for library: ${libraryId}`); + logger.info(`Triggering ${backendMode} library scan for library: ${libraryId}`); return await this.jobQueue.addPlexScanJob( libraryId || '', @@ -438,7 +441,7 @@ export class SchedulerService { * Check for overdue jobs and trigger them */ private async triggerOverdueJobs(): Promise { - console.log('[Scheduler] Checking for overdue jobs...'); + logger.info('Checking for overdue jobs...'); const jobs = await prisma.scheduledJob.findMany({ where: { enabled: true }, @@ -447,11 +450,11 @@ export class SchedulerService { for (const job of jobs) { try { if (this.isJobOverdue(job)) { - console.log(`[Scheduler] Job "${job.name}" is overdue, triggering now...`); + logger.info(`Job "${job.name}" is overdue, triggering now...`); await this.triggerJobNow(job.id); } } catch (error) { - console.error(`[Scheduler] Failed to trigger overdue job "${job.name}":`, error); + logger.error(`Failed to trigger overdue job "${job.name}"`, { error: error instanceof Error ? error.message : String(error) }); } } } @@ -468,7 +471,7 @@ export class SchedulerService { // Parse cron expression to get interval in milliseconds const intervalMs = this.getIntervalFromCron(job.schedule); if (!intervalMs) { - console.warn(`[Scheduler] Could not parse interval for job "${job.name}", skipping`); + logger.warn(`Could not parse interval for job "${job.name}", skipping`); return false; } @@ -530,7 +533,7 @@ export class SchedulerService { } // For other patterns, return a conservative default (24 hours) - console.warn(`[Scheduler] Unknown cron pattern "${cronExpression}", defaulting to 24 hours`); + logger.warn(`Unknown cron pattern "${cronExpression}", defaulting to 24 hours`); return 24 * 60 * 60 * 1000; } diff --git a/src/lib/services/thumbnail-cache.service.ts b/src/lib/services/thumbnail-cache.service.ts index 5b41fb4..8053ffc 100644 --- a/src/lib/services/thumbnail-cache.service.ts +++ b/src/lib/services/thumbnail-cache.service.ts @@ -7,6 +7,9 @@ import fs from 'fs/promises'; import path from 'path'; import crypto from 'crypto'; import axios from 'axios'; +import { RMABLogger } from '../utils/logger'; + +const logger = RMABLogger.create('ThumbnailCache'); const CACHE_DIR = '/app/cache/thumbnails'; const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB max per image @@ -20,7 +23,7 @@ export class ThumbnailCacheService { try { await fs.mkdir(CACHE_DIR, { recursive: true }); } catch (error) { - console.error('[ThumbnailCache] Failed to create cache directory:', error); + logger.error('Failed to create cache directory', { error: error instanceof Error ? error.message : String(error) }); throw error; } } @@ -79,18 +82,18 @@ export class ThumbnailCacheService { // Verify content type is an image const contentType = response.headers['content-type']; if (!contentType || !contentType.startsWith('image/')) { - console.warn(`[ThumbnailCache] Invalid content type for ${asin}: ${contentType}`); + logger.warn(`Invalid content type for ${asin}: ${contentType}`); return null; } // Write to file await fs.writeFile(filePath, Buffer.from(response.data)); - console.log(`[ThumbnailCache] Cached thumbnail for ${asin}: ${filePath}`); + logger.info(`Cached thumbnail for ${asin}: ${filePath}`); return filePath; } catch (error) { // Log error but don't throw - we'll fall back to the original URL - console.error(`[ThumbnailCache] Failed to cache thumbnail for ${asin}:`, error); + logger.error(`Failed to cache thumbnail for ${asin}`, { error: error instanceof Error ? error.message : String(error) }); return null; } } @@ -108,10 +111,10 @@ export class ThumbnailCacheService { for (const file of asinFiles) { const filePath = path.join(CACHE_DIR, file); await fs.unlink(filePath); - console.log(`[ThumbnailCache] Deleted thumbnail: ${filePath}`); + logger.info(`Deleted thumbnail: ${filePath}`); } } catch (error) { - console.error(`[ThumbnailCache] Failed to delete thumbnail for ${asin}:`, error); + logger.error(`Failed to delete thumbnail for ${asin}`, { error: error instanceof Error ? error.message : String(error) }); } } @@ -135,14 +138,14 @@ export class ThumbnailCacheService { const filePath = path.join(CACHE_DIR, file); await fs.unlink(filePath); deletedCount++; - console.log(`[ThumbnailCache] Deleted unused thumbnail: ${file}`); + logger.info(`Deleted unused thumbnail: ${file}`); } } - console.log(`[ThumbnailCache] Cleanup complete: ${deletedCount} thumbnails deleted`); + logger.info(`Cleanup complete: ${deletedCount} thumbnails deleted`); return deletedCount; } catch (error) { - console.error('[ThumbnailCache] Failed to cleanup thumbnails:', error); + logger.error('Failed to cleanup thumbnails', { error: error instanceof Error ? error.message : String(error) }); return 0; } } diff --git a/src/lib/utils/audiobook-matcher.ts b/src/lib/utils/audiobook-matcher.ts index 8141b5e..8d00ef8 100644 --- a/src/lib/utils/audiobook-matcher.ts +++ b/src/lib/utils/audiobook-matcher.ts @@ -9,9 +9,10 @@ import { prisma } from '@/lib/db'; import { compareTwoStrings } from 'string-similarity'; import { LibraryItem } from '@/lib/services/library'; +import { RMABLogger } from './logger'; -// Debug logging controlled by LOG_LEVEL environment variable -const DEBUG_ENABLED = process.env.LOG_LEVEL === 'debug'; +// Module-level logger +const logger = RMABLogger.create('AudiobookMatcher'); export interface AudiobookMatchInput { asin: string; @@ -109,7 +110,7 @@ export async function findPlexMatch( // If no candidates found, log and return null if (plexBooks.length === 0) { matchResult.matchType = 'no_candidates'; - if (DEBUG_ENABLED) console.log(JSON.stringify({ MATCHER: matchResult })); + logger.debug('Matcher result', { MATCHER: matchResult }); return null; } @@ -125,7 +126,7 @@ export async function findPlexMatch( asin: plexBook.asin, confidence: 100, }; - if (DEBUG_ENABLED) console.log(JSON.stringify({ MATCHER: matchResult })); + logger.debug('Matcher result', { MATCHER: matchResult }); return plexBook; } } @@ -141,7 +142,7 @@ export async function findPlexMatch( plexAuthor: plexBook.author, confidence: 100, }; - if (DEBUG_ENABLED) console.log(JSON.stringify({ MATCHER: matchResult })); + logger.debug('Matcher result', { MATCHER: matchResult }); return plexBook; } } @@ -182,7 +183,7 @@ export async function findPlexMatch( if (validCandidates.length === 0) { matchResult.matchType = 'asin_filtered_all'; - if (DEBUG_ENABLED) console.log(JSON.stringify({ MATCHER: matchResult })); + logger.debug('Matcher result', { MATCHER: matchResult }); return null; } @@ -250,13 +251,13 @@ export async function findPlexMatch( plexAuthor: bestMatch.plexBook.author, confidence: Math.round(bestMatch.score * 100), }; - if (DEBUG_ENABLED) console.log(JSON.stringify({ MATCHER: matchResult })); + logger.debug('Matcher result', { MATCHER: matchResult }); return bestMatch.plexBook; } // No match found matchResult.matchType = 'fuzzy_below_threshold'; - if (DEBUG_ENABLED) console.log(JSON.stringify({ MATCHER: matchResult })); + logger.debug('Matcher result', { MATCHER: matchResult }); return null; } @@ -362,15 +363,12 @@ export async function enrichAudiobooksWithMatches( } } - if (DEBUG_ENABLED) { - const summary = { - total: results.length, - available: results.filter(r => r.isAvailable).length, - notAvailable: results.filter(r => !r.isAvailable).length, - requested: userId ? results.filter(r => (r as any).isRequested).length : 'N/A', - }; - console.log(JSON.stringify({ MATCHER_BATCH_SUMMARY: summary })); - } + logger.debug('Batch summary', { + total: results.length, + available: results.filter(r => r.isAvailable).length, + notAvailable: results.filter(r => !r.isAvailable).length, + requested: userId ? results.filter(r => (r as any).isRequested).length : 'N/A', + }); return results; } @@ -405,16 +403,12 @@ export function matchAudiobook( item.asin?.toLowerCase() === request.asin?.toLowerCase() ); if (asinMatch) { - if (DEBUG_ENABLED) { - console.log(JSON.stringify({ - GENERIC_MATCHER: { - matchType: 'asin_exact', - input: { title: request.title, asin: request.asin }, - matched: { title: asinMatch.title, asin: asinMatch.asin }, - confidence: 100 - } - })); - } + logger.debug('Generic matcher result', { + matchType: 'asin_exact', + input: { title: request.title, asin: request.asin }, + matched: { title: asinMatch.title, asin: asinMatch.asin }, + confidence: 100 + }); return asinMatch; } } @@ -426,16 +420,12 @@ export function matchAudiobook( item.isbn && normalizeISBN(item.isbn) === normalizedRequestISBN ); if (isbnMatch) { - if (DEBUG_ENABLED) { - console.log(JSON.stringify({ - GENERIC_MATCHER: { - matchType: 'isbn_exact', - input: { title: request.title, isbn: request.isbn }, - matched: { title: isbnMatch.title, isbn: isbnMatch.isbn }, - confidence: 95 - } - })); - } + logger.debug('Generic matcher result', { + matchType: 'isbn_exact', + input: { title: request.title, isbn: request.isbn }, + matched: { title: isbnMatch.title, isbn: isbnMatch.isbn }, + confidence: 95 + }); return isbnMatch; } } @@ -463,35 +453,27 @@ export function matchAudiobook( // Accept if score >= 70% if (bestMatch && bestMatch.score >= 0.7) { - if (DEBUG_ENABLED) { - console.log(JSON.stringify({ - GENERIC_MATCHER: { - matchType: 'fuzzy', - input: { title: request.title, author: request.author }, - matched: { title: bestMatch.item.title, author: bestMatch.item.author }, - scores: { - title: Math.round(bestMatch.titleScore * 100), - author: Math.round(bestMatch.authorScore * 100), - overall: Math.round(bestMatch.score * 100) - }, - confidence: Math.round(bestMatch.score * 100) - } - })); - } + logger.debug('Generic matcher result', { + matchType: 'fuzzy', + input: { title: request.title, author: request.author }, + matched: { title: bestMatch.item.title, author: bestMatch.item.author }, + scores: { + title: Math.round(bestMatch.titleScore * 100), + author: Math.round(bestMatch.authorScore * 100), + overall: Math.round(bestMatch.score * 100) + }, + confidence: Math.round(bestMatch.score * 100) + }); return bestMatch.item; } // No match found - if (DEBUG_ENABLED) { - console.log(JSON.stringify({ - GENERIC_MATCHER: { - matchType: 'no_match', - input: { title: request.title, author: request.author }, - bestScore: bestMatch ? Math.round(bestMatch.score * 100) : 0, - threshold: 70 - } - })); - } + logger.debug('Generic matcher result', { + matchType: 'no_match', + input: { title: request.title, author: request.author }, + bestScore: bestMatch ? Math.round(bestMatch.score * 100) : 0, + threshold: 70 + }); return null; } diff --git a/src/lib/utils/file-organizer.ts b/src/lib/utils/file-organizer.ts index d940bf4..957648c 100644 --- a/src/lib/utils/file-organizer.ts +++ b/src/lib/utils/file-organizer.ts @@ -8,6 +8,9 @@ import path from 'path'; import axios from 'axios'; import { createJobLogger, JobLogger } from './job-logger'; import { tagMultipleFiles, checkFfmpegAvailable } from './metadata-tagger'; +import { RMABLogger } from './logger'; + +const moduleLogger = RMABLogger.create('FileOrganizer'); import { detectChapterFiles, analyzeChapterFiles, @@ -296,7 +299,7 @@ export class FileOrganizer { try { await fs.access(sourcePath, fs.constants.R_OK); } catch { - console.warn(`[FileOrganizer] Source file not found or not readable: ${sourcePath}`); + moduleLogger.warn(`Source file not found or not readable: ${sourcePath}`); result.errors.push(`Source file not found: ${audioFile}`); continue; } @@ -304,7 +307,7 @@ export class FileOrganizer { // Check if target already exists (skip if already copied) try { await fs.access(targetFilePath); - console.log(`[FileOrganizer] File already exists, skipping: ${filename}`); + moduleLogger.debug(`File already exists, skipping: ${filename}`); result.audioFiles.push(targetFilePath); // Clean up tagged temp file if it exists @@ -504,7 +507,7 @@ export class FileOrganizer { } } } catch (error) { - console.error('[FileOrganizer] Error reading directory:', error); + moduleLogger.error('Error reading directory', { error: error instanceof Error ? error.message : String(error) }); throw error; } @@ -532,7 +535,7 @@ export class FileOrganizer { } } } catch (error) { - console.error(`[FileOrganizer] Error reading directory ${dir}:`, error); + moduleLogger.error(`Error reading directory ${dir}`, { error: error instanceof Error ? error.message : String(error) }); } return files; @@ -601,7 +604,7 @@ export class FileOrganizer { // Copy from local cache instead of downloading await fs.copyFile(cachedPath, targetPath); await fs.chmod(targetPath, 0o644); - console.log(`[FileOrganizer] Copied cover art from cache: ${filename}`); + moduleLogger.debug(`Copied cover art from cache: ${filename}`); } else { // Download from external URL (e.g., Audible CDN) const response = await axios.get(url, { @@ -610,10 +613,10 @@ export class FileOrganizer { }); await fs.writeFile(targetPath, response.data); - console.log(`[FileOrganizer] Downloaded cover art from URL`); + moduleLogger.debug(`Downloaded cover art from URL`); } } catch (error) { - console.error('[FileOrganizer] Failed to download cover art:', error); + moduleLogger.error('Failed to download cover art', { error: error instanceof Error ? error.message : String(error) }); throw error; } } @@ -625,9 +628,9 @@ export class FileOrganizer { try { // Remove download directory and all remaining files await fs.rm(downloadPath, { recursive: true, force: true }); - console.log(`[FileOrganizer] Cleaned up: ${downloadPath}`); + moduleLogger.debug(`Cleaned up: ${downloadPath}`); } catch (error) { - console.error(`[FileOrganizer] Cleanup failed for ${downloadPath}:`, error); + moduleLogger.error(`Cleanup failed for ${downloadPath}`, { error: error instanceof Error ? error.message : String(error) }); // Don't throw - cleanup is non-critical } } diff --git a/src/lib/utils/job-logger.ts b/src/lib/utils/job-logger.ts index 2c899da..50bb8d6 100644 --- a/src/lib/utils/job-logger.ts +++ b/src/lib/utils/job-logger.ts @@ -1,96 +1,65 @@ /** - * Component: Job Logger Utility + * Component: Job Logger Utility (Backward Compatibility) * Documentation: documentation/backend/services/jobs.md * - * Provides structured logging for job processors with database persistence + * @deprecated Use RMABLogger.forJob() directly for new code. + * This file provides backward compatibility for existing processors. + * + * Migration example: + * ```typescript + * // Before (deprecated) + * const logger = jobId ? createJobLogger(jobId, 'Context') : null; + * await logger?.info('message'); + * + * // After (preferred) + * import { RMABLogger } from './logger'; + * const logger = RMABLogger.forJob(jobId, 'Context'); + * logger.info('message'); // No await needed! + * ``` */ -import { prisma } from '../db'; +import { RMABLogger, LogMetadata } from './logger'; export type LogLevel = 'info' | 'warn' | 'error'; -export interface LogMetadata { - [key: string]: any; -} - /** - * Job Logger - Logs events to both console and database + * @deprecated Use RMABLogger.forJob() directly */ export class JobLogger { - private jobId: string; - private context: string; + private logger: RMABLogger; constructor(jobId: string, context: string) { - this.jobId = jobId; - this.context = context; + this.logger = RMABLogger.forJob(jobId, context); } /** * Log info message + * @deprecated Returns Promise for backward compat but is actually synchronous */ async info(message: string, metadata?: LogMetadata): Promise { - await this.log('info', message, metadata); + this.logger.info(message, metadata); } /** * Log warning message + * @deprecated Returns Promise for backward compat but is actually synchronous */ async warn(message: string, metadata?: LogMetadata): Promise { - await this.log('warn', message, metadata); + this.logger.warn(message, metadata); } /** * Log error message + * @deprecated Returns Promise for backward compat but is actually synchronous */ async error(message: string, metadata?: LogMetadata): Promise { - await this.log('error', message, metadata); - } - - /** - * Internal logging method - */ - private async log(level: LogLevel, message: string, metadata?: LogMetadata): Promise { - // Log to console with timestamp (for Docker logs) - const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; - const consoleMessage = `[${this.context}] ${message}`; - - switch (level) { - case 'info': - console.log(consoleMessage); - break; - case 'warn': - console.warn(consoleMessage); - break; - case 'error': - console.error(consoleMessage); - break; - } - - // Log metadata if provided - if (metadata && Object.keys(metadata).length > 0) { - console.log(timestamp, JSON.stringify(metadata, null, 2)); - } - - // Persist to database (non-blocking, ignore errors to not break job execution) - try { - await prisma.jobEvent.create({ - data: { - jobId: this.jobId, - level, - context: this.context, - message, - metadata: metadata ? JSON.parse(JSON.stringify(metadata)) : null, - }, - }); - } catch (error) { - console.error('[JobLogger] Failed to persist log to database:', error); - // Don't throw - logging failure should not break job execution - } + this.logger.error(message, metadata); } } /** * Create a job logger instance + * @deprecated Use RMABLogger.forJob() directly */ export function createJobLogger(jobId: string, context: string): JobLogger { return new JobLogger(jobId, context); diff --git a/src/lib/utils/jwt-client.ts b/src/lib/utils/jwt-client.ts index c9a43f3..2268b31 100644 --- a/src/lib/utils/jwt-client.ts +++ b/src/lib/utils/jwt-client.ts @@ -3,6 +3,10 @@ * Documentation: documentation/frontend/routing-auth.md */ +import { RMABLogger } from './logger'; + +const logger = RMABLogger.create('JWTClient'); + interface JWTPayload { sub: string; plexId: string; @@ -27,7 +31,7 @@ export function decodeJWT(token: string): JWTPayload | null { const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/'))); return decoded as JWTPayload; } catch (error) { - console.error('Failed to decode JWT:', error); + logger.error('Failed to decode JWT', { error: error instanceof Error ? error.message : String(error) }); return null; } } diff --git a/src/lib/utils/jwt.ts b/src/lib/utils/jwt.ts index 2d99ecd..c9f07f6 100644 --- a/src/lib/utils/jwt.ts +++ b/src/lib/utils/jwt.ts @@ -4,6 +4,9 @@ */ import jwt from 'jsonwebtoken'; +import { RMABLogger } from './logger'; + +const logger = RMABLogger.create('JWT'); const JWT_SECRET = process.env.JWT_SECRET || 'change-this-to-a-random-secret-key'; const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'change-this-to-another-random-secret-key'; @@ -54,10 +57,7 @@ export function verifyAccessToken(token: string): TokenPayload | null { const decoded = jwt.verify(token, JWT_SECRET) as TokenPayload; return decoded; } catch (error) { - console.error('[JWT] Access token verification failed:', error); - if (error instanceof Error) { - console.error('[JWT] Error details:', error.message); - } + logger.error('Access token verification failed', { error: error instanceof Error ? error.message : String(error) }); return null; } } @@ -73,7 +73,7 @@ export function verifyRefreshToken(token: string): RefreshTokenPayload | null { } return decoded; } catch (error) { - console.error('Refresh token verification failed:', error); + logger.error('Refresh token verification failed', { error: error instanceof Error ? error.message : String(error) }); return null; } } diff --git a/src/lib/utils/logger.ts b/src/lib/utils/logger.ts new file mode 100644 index 0000000..4400228 --- /dev/null +++ b/src/lib/utils/logger.ts @@ -0,0 +1,213 @@ +/** + * Component: Centralized Logging System (RMABLogger) + * Documentation: documentation/backend/services/logging.md + * + * Single logging infrastructure for all console and database logging. + * All logs in the application should go through RMABLogger. + */ + +import { prisma } from '../db'; + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'quiet'; + +export interface LogMetadata { + [key: string]: unknown; +} + +// Log level hierarchy (lower number = more verbose) +const LEVEL_PRIORITY: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + quiet: 4, +}; + +/** + * Get configured log level from environment (single source of truth) + */ +function getConfiguredLogLevel(): LogLevel { + const envLevel = process.env.LOG_LEVEL?.toLowerCase(); + if (envLevel && envLevel in LEVEL_PRIORITY) { + return envLevel as LogLevel; + } + return 'info'; // Default +} + +// Cached log level (computed once at module load) +const CONFIGURED_LOG_LEVEL = getConfiguredLogLevel(); +const CONFIGURED_LOG_PRIORITY = LEVEL_PRIORITY[CONFIGURED_LOG_LEVEL]; + +/** + * RMABLogger - Centralized Logger for ReadMeABook + * + * Features: + * - Context namespacing (e.g., RMABLogger.create('QBittorrent')) + * - Job-aware database persistence (e.g., RMABLogger.forJob(jobId, 'Context')) + * - Single LOG_LEVEL env var check point + * - Consistent formatting: [LEVEL] [Context] Message + * - Synchronous API - no await needed + * + * Usage: + * ```typescript + * // Standard logging + * const logger = RMABLogger.create('QBittorrent'); + * logger.info('Connected successfully'); + * logger.debug('Cookie value', { cookie: '...' }); + * + * // Job-aware logging (persists to database) + * const logger = RMABLogger.forJob(jobId, 'SearchIndexers'); + * logger.info('Processing request'); // Logs to console AND database + * ``` + */ +export class RMABLogger { + private context: string; + private jobId: string | undefined; + + private constructor(context: string, jobId?: string) { + this.context = context; + this.jobId = jobId; + } + + /** + * Create a new logger with context namespace + * @param context - Logger context (e.g., 'QBittorrent', 'Plex', 'API.Auth') + */ + static create(context: string): RMABLogger { + return new RMABLogger(context); + } + + /** + * Create a job-aware logger that persists to database + * @param jobId - Job ID for database persistence (if undefined, logs to console only) + * @param context - Logger context (e.g., 'SearchIndexers', 'MonitorDownload') + */ + static forJob(jobId: string | undefined, context: string): RMABLogger { + return new RMABLogger(context, jobId); + } + + /** + * Create a child logger with extended context + * @param subContext - Additional context to append + */ + child(subContext: string): RMABLogger { + return new RMABLogger(`${this.context}.${subContext}`, this.jobId); + } + + /** + * Debug level logging (most verbose) + * Only logged when LOG_LEVEL=debug + * Never persisted to database + */ + debug(message: string, metadata?: LogMetadata): void { + this.log('debug', message, metadata); + } + + /** + * Info level logging (default level) + * Logged unless LOG_LEVEL=warn, error, or quiet + */ + info(message: string, metadata?: LogMetadata): void { + this.log('info', message, metadata); + } + + /** + * Warning level logging + * Logged unless LOG_LEVEL=error or quiet + */ + warn(message: string, metadata?: LogMetadata): void { + this.log('warn', message, metadata); + } + + /** + * Error level logging + * Always logged unless LOG_LEVEL=quiet + */ + error(message: string, metadata?: LogMetadata): void { + this.log('error', message, metadata); + } + + /** + * Internal logging method - single point of LOG_LEVEL checking + */ + private log( + level: Exclude, + message: string, + metadata?: LogMetadata + ): void { + const levelPriority = LEVEL_PRIORITY[level]; + + // Check if this level should be logged (single check point) + if (levelPriority < CONFIGURED_LOG_PRIORITY) { + return; + } + + // Format: [LEVEL] [Context] Message + const formattedMessage = `[${level.toUpperCase()}] [${this.context}] ${message}`; + + // Console output using appropriate method + switch (level) { + case 'debug': + console.debug(formattedMessage); + break; + case 'info': + console.log(formattedMessage); + break; + case 'warn': + console.warn(formattedMessage); + break; + case 'error': + console.error(formattedMessage); + break; + } + + // Log metadata if provided + if (metadata && Object.keys(metadata).length > 0) { + console.log(JSON.stringify(metadata, null, 2)); + } + + // Persist to database for job-aware loggers (fire-and-forget) + // Debug logs are NEVER persisted to keep job_events clean + if (this.jobId && level !== 'debug') { + this.persistToDatabase(level, message, metadata); + } + } + + /** + * Persist log to database (non-blocking, fire-and-forget) + * Errors are silently caught - logging should never break job execution + */ + private persistToDatabase( + level: Exclude, + message: string, + metadata?: LogMetadata + ): void { + prisma.jobEvent + .create({ + data: { + jobId: this.jobId!, + level, + context: this.context, + message, + metadata: metadata ? JSON.parse(JSON.stringify(metadata)) : null, + }, + }) + .catch(() => { + // Silently fail - logging should never break job execution + }); + } +} + +/** + * Convenience function to get the current log level + */ +export function getLogLevel(): LogLevel { + return CONFIGURED_LOG_LEVEL; +} + +/** + * Check if debug logging is enabled + */ +export function isDebugEnabled(): boolean { + return CONFIGURED_LOG_LEVEL === 'debug'; +} diff --git a/src/lib/utils/path-mapper.ts b/src/lib/utils/path-mapper.ts index b18986d..6b9a552 100644 --- a/src/lib/utils/path-mapper.ts +++ b/src/lib/utils/path-mapper.ts @@ -7,6 +7,9 @@ */ import path from 'path'; +import { RMABLogger } from './logger'; + +const logger = RMABLogger.create('PathMapper'); export interface PathMappingConfig { enabled: boolean; @@ -35,7 +38,7 @@ export class PathMapper { // 2. Handle empty paths if (!qbittorrentPath || !config.remotePath || !config.localPath) { - console.warn('PathMapper: Empty path or config, returning original'); + logger.warn('Empty path or config, returning original'); return qbittorrentPath; } @@ -47,8 +50,8 @@ export class PathMapper { // 4. Check if qBittorrent path starts with remote path if (!normalizedQbPath.startsWith(normalizedRemote)) { - console.warn( - `PathMapper: Path "${qbittorrentPath}" does not start with remote path "${config.remotePath}". ` + + logger.warn( + `Path "${qbittorrentPath}" does not start with remote path "${config.remotePath}". ` + `Returning original path unchanged.` ); return qbittorrentPath; @@ -60,7 +63,7 @@ export class PathMapper { // Join local path with relative path, ensuring proper path separators const transformedPath = path.join(normalizedLocal, relativePath); - console.log(`PathMapper: Transformed "${qbittorrentPath}" → "${transformedPath}"`); + logger.info(`Transformed "${qbittorrentPath}" to "${transformedPath}"`); return transformedPath; } @@ -95,7 +98,7 @@ export class PathMapper { // Warn if paths look suspicious (but don't throw) if (config.remotePath === config.localPath) { - console.warn('PathMapper: Remote and local paths are identical - path mapping will have no effect'); + logger.warn('Remote and local paths are identical - path mapping will have no effect'); } } diff --git a/src/lib/utils/url.ts b/src/lib/utils/url.ts index 7e2156c..5c7a07a 100644 --- a/src/lib/utils/url.ts +++ b/src/lib/utils/url.ts @@ -3,6 +3,10 @@ * Documentation: documentation/backend/services/environment.md */ +import { RMABLogger } from './logger'; + +const logger = RMABLogger.create('URL'); + /** * Get application base URL for OAuth callbacks and redirects * @@ -35,22 +39,20 @@ export function getBaseUrl(): string { // Validate URL format if (!url.startsWith('http://') && !url.startsWith('https://')) { - console.warn(`[URL Utility] Invalid base URL format: ${url}. URLs must start with http:// or https://`); + logger.warn(`Invalid base URL format: ${url}. URLs must start with http:// or https://`); } // Production warning if using localhost if (process.env.NODE_ENV === 'production' && url.includes('localhost')) { - console.warn('[URL Utility] ⚠️ WARNING: Using localhost URL in production. OAuth callbacks may fail. Set PUBLIC_URL environment variable.'); + logger.warn('Using localhost URL in production. OAuth callbacks may fail. Set PUBLIC_URL environment variable.'); } // Log which variable is being used (debug only) - if (process.env.LOG_LEVEL === 'debug') { - const source = publicUrl ? 'PUBLIC_URL' : - nextAuthUrl ? 'NEXTAUTH_URL' : - baseUrl ? 'BASE_URL' : - 'default (localhost)'; - console.debug(`[URL Utility] Using base URL from ${source}: ${url}`); - } + const source = publicUrl ? 'PUBLIC_URL' : + nextAuthUrl ? 'NEXTAUTH_URL' : + baseUrl ? 'BASE_URL' : + 'default (localhost)'; + logger.debug(`Using base URL from ${source}: ${url}`); return url; }