mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
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:
@@ -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) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user