mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 21:00: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:
@@ -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<AudibleAudiobook[]> {
|
||||
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<AudibleAudiobook[]> {
|
||||
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<AudibleSearchResult> {
|
||||
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<AudibleAudiobook | null> {
|
||||
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<AudibleAudiobook | null> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string | null> {
|
||||
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<boolean> {
|
||||
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<PlexHomeUser[]> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, number>, 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<string, number>);
|
||||
console.log(`[Prowlarr] Protocol distribution in ${results.length} results:`, JSON.stringify(protocolCounts));
|
||||
logger.debug(`Protocol distribution in ${results.length} results`, { protocols: protocolCounts });
|
||||
|
||||
// Debug: Log first few results to see their protocols
|
||||
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<ProwlarrService> {
|
||||
// Test connection
|
||||
const isConnected = await prowlarrService.testConnection();
|
||||
if (!isConnected) {
|
||||
console.warn('Warning: Prowlarr connection test failed');
|
||||
logger.warn('Connection test failed');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string> {
|
||||
// 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<string> {
|
||||
console.log(`[qBittorrent] Downloading .torrent file from: ${torrentUrl}`);
|
||||
logger.info(` Downloading .torrent file from: ${torrentUrl}`);
|
||||
|
||||
// Make initial request with maxRedirects: 0 to intercept redirects
|
||||
// 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<QBittorrentService> {
|
||||
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<QBittorrentService> {
|
||||
'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<QBittorrentService> {
|
||||
|
||||
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<QBittorrentService> {
|
||||
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<QBittorrentService> {
|
||||
);
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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<string> {
|
||||
console.log(`[SABnzbd] Adding NZB from URL: ${url.substring(0, 150)}...`);
|
||||
logger.info(`Adding NZB from URL: ${url.substring(0, 150)}...`);
|
||||
|
||||
const response = await this.client.get('/api', {
|
||||
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<SABnzbdService> {
|
||||
|
||||
export function invalidateSABnzbdService(): void {
|
||||
sabnzbdServiceInstance = null;
|
||||
console.log('[SABnzbd] Service singleton invalidated');
|
||||
logger.info('Service singleton invalidated');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user