Implement centralized logging with RMABLogger

Replaces scattered console statements with a unified RMABLogger across backend API routes and services. Adds LOG_LEVEL-based filtering, job-aware database persistence, and context-based logging. Updates documentation to describe the new logging system and usage patterns. Also documents qBittorrent CSRF header fix
This commit is contained in:
kikootwo
2026-01-12 12:45:48 -05:00
parent ba5f5cf7d6
commit 682836237b
118 changed files with 1623 additions and 1079 deletions
+4 -1
View File
@@ -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) });
}
}
+10 -7
View File
@@ -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;
}
}
+22 -21
View File
@@ -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;
}
}
+6 -3
View File
@@ -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;
}
}
+7 -4
View File
@@ -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;
}
}
+48 -85
View File
@@ -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<string, string | null>();
@@ -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., <li><a>Slow Partner Server #5</a> (no waitlist, but can be very slow)</li>
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 [];
}
}
+15 -12
View File
@@ -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();
}
});
@@ -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;
}
}
+41 -38
View File
@@ -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 {
+20 -17
View File
@@ -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<void> {
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<void> {
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;
}
+12 -9
View File
@@ -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;
}
}