novel-reader-sdk
Version:
SDK for Novel Reader API
415 lines (352 loc) • 10.2 kB
text/typescript
/**
* Scraping Service SDK Client
*
* Type-safe client for consuming the scraping service API
* Provides methods for all endpoints with proper error handling
*/
import type {
// Core types
NovelDetails,
ChapterData,
NovelSearchResult,
SiteConfiguration,
HealthStatus,
DatabaseInfo,
DatabaseId,
// Health types
HealthOverview,
ProxyHealthStatus,
BrowserHealthStatus,
ServiceStatus,
// Request types
SearchNovelsRequest,
ScrapeChapterRequest,
BatchScrapeRequest,
ValidateUrlRequest,
TestSiteConfigRequest,
LatestNovelsRequest,
PopularNovelsRequest,
NovelDetailsRequest,
// Response types
ScrapingServiceResponse,
PaginatedResponse,
} from './types';
import {
fetchWithTimeout,
parseJsonResponse,
buildFullUrl,
validateDatabaseId,
logRequest,
logResponse,
withRetry,
type RetryConfig,
} from './utils';
import {
ValidationError,
} from './errors';
// ===== Client Configuration =====
export interface ScrapingServiceConfig {
/** Base URL of the scraping service */
baseUrl: string;
/** Request timeout in milliseconds */
timeout?: number;
/** Retry configuration for failed requests */
retry?: Partial<RetryConfig>;
/** Enable debug logging */
debug?: boolean;
}
// ===== Main Client Class =====
export class ScrapingServiceClient {
private readonly config: Required<ScrapingServiceConfig>;
constructor(config: ScrapingServiceConfig) {
this.config = {
timeout: 30000,
retry: {},
debug: false,
...config,
};
// Ensure base URL doesn't end with slash
this.config.baseUrl = this.config.baseUrl.replace(/\/$/, '');
}
// ===== Database Operations =====
/**
* List available database scrapers
*/
async listDatabases(): Promise<DatabaseInfo[]> {
return this.makeRequest<ScrapingServiceResponse<DatabaseInfo[]>>({
method: 'GET',
path: '/api/v1/databases',
}).then(response => response.results);
}
/**
* Search novels in a database
*/
async searchNovels(
databaseId: DatabaseId,
request: SearchNovelsRequest
): Promise<PaginatedResponse<NovelSearchResult>> {
validateDatabaseId(databaseId);
return this.makeRequest<PaginatedResponse<NovelSearchResult>>({
method: 'GET',
path: '/api/v1/databases/:id/search',
params: { id: databaseId },
query: request,
});
}
/**
* Get latest novels from a database
*/
async getLatestNovels(
databaseId: DatabaseId,
request: LatestNovelsRequest = {}
): Promise<PaginatedResponse<NovelSearchResult>> {
validateDatabaseId(databaseId);
return this.makeRequest<PaginatedResponse<NovelSearchResult>>({
method: 'GET',
path: '/api/v1/databases/:id/latest',
params: { id: databaseId },
query: request,
});
}
/**
* Get popular novels from a database
*/
async getPopularNovels(
databaseId: DatabaseId,
request: PopularNovelsRequest = {}
): Promise<PaginatedResponse<NovelSearchResult>> {
validateDatabaseId(databaseId);
return this.makeRequest<PaginatedResponse<NovelSearchResult>>({
method: 'GET',
path: '/api/v1/databases/:id/popular',
params: { id: databaseId },
query: request,
});
}
/**
* Get detailed novel information
*/
async getNovelDetails(
databaseId: DatabaseId,
request: NovelDetailsRequest
): Promise<NovelDetails> {
validateDatabaseId(databaseId);
if (!request.url) {
throw new ValidationError('Novel URL is required');
}
return this.makeRequest<ScrapingServiceResponse<NovelDetails>>({
method: 'GET',
path: '/api/v1/databases/:id/novel',
params: { id: databaseId },
query: request,
}).then(response => response.results);
}
// ===== Chapter Operations =====
/**
* Scrape single chapter content
*/
async scrapeChapter(request: ScrapeChapterRequest): Promise<ChapterData> {
if (!request.url) {
throw new ValidationError('Chapter URL is required');
}
return this.makeRequest<ScrapingServiceResponse<ChapterData>>({
method: 'POST',
path: '/api/v1/chapters/scrape',
body: request,
}).then(response => response.results);
}
/**
* Scrape multiple chapters in batch
*/
async scrapeChaptersBatch(request: BatchScrapeRequest): Promise<ChapterData[]> {
if (!request.urls || request.urls.length === 0) {
throw new ValidationError('At least one URL is required');
}
return this.makeRequest<ScrapingServiceResponse<ChapterData[]>>({
method: 'POST',
path: '/api/v1/chapters/batch',
body: request,
}).then(response => response.results);
}
/**
* Validate if URL is supported for scraping
*/
async validateChapterUrl(request: ValidateUrlRequest): Promise<{ supported: boolean; hostname: string }> {
if (!request.url) {
throw new ValidationError('URL is required');
}
return this.makeRequest<ScrapingServiceResponse<{ supported: boolean; hostname: string }>>({
method: 'GET',
path: '/api/v1/chapters/validate-url',
query: request,
}).then(response => response.results);
}
// ===== Site Configuration Operations =====
/**
* List all site configurations
*/
async listSiteConfigurations(): Promise<SiteConfiguration[]> {
return this.makeRequest<ScrapingServiceResponse<SiteConfiguration[]>>({
method: 'GET',
path: '/api/v1/sites',
}).then(response => response.results);
}
/**
* Get site configuration by hostname
*/
async getSiteConfiguration(hostname: string): Promise<SiteConfiguration> {
if (!hostname) {
throw new ValidationError('Hostname is required');
}
return this.makeRequest<ScrapingServiceResponse<SiteConfiguration>>({
method: 'GET',
path: '/api/v1/sites/:hostname',
params: { hostname },
}).then(response => response.results);
}
/**
* Update site configuration
*/
async updateSiteConfiguration(
hostname: string,
update: Partial<SiteConfiguration>
): Promise<SiteConfiguration> {
if (!hostname) {
throw new ValidationError('Hostname is required');
}
return this.makeRequest<ScrapingServiceResponse<SiteConfiguration>>({
method: 'PUT',
path: '/api/v1/sites/:hostname',
params: { hostname },
body: update,
}).then(response => response.results);
}
/**
* Test site configuration
*/
async testSiteConfiguration(
hostname: string,
request: TestSiteConfigRequest
): Promise<{ success: boolean; data?: ChapterData }> {
if (!hostname) {
throw new ValidationError('Hostname is required');
}
if (!request.testUrl) {
throw new ValidationError('Test URL is required');
}
return this.makeRequest<ScrapingServiceResponse<{ success: boolean; data?: ChapterData }>>({
method: 'POST',
path: '/api/v1/sites/:hostname/test',
params: { hostname },
body: request,
}).then(response => response.results);
}
// ===== Health Operations =====
/**
* Get overall service health status
* @deprecated Use getHealthOverview() for comprehensive health data
*/
async getHealthStatus(): Promise<HealthStatus> {
return this.makeRequest<HealthStatus>({
method: 'GET',
path: '/api/v1/health',
});
}
/**
* Get comprehensive health overview for dashboards
*/
async getHealthOverview(): Promise<HealthOverview> {
return this.makeRequest<HealthOverview>({
method: 'GET',
path: '/api/v1/health/overview',
});
}
/**
* Get detailed proxy health status
*/
async getProxyHealth(): Promise<ProxyHealthStatus> {
return this.makeRequest<ProxyHealthStatus>({
method: 'GET',
path: '/api/v1/health/proxy',
});
}
/**
* Get browser manager health status
*/
async getBrowserHealth(): Promise<BrowserHealthStatus> {
return this.makeRequest<BrowserHealthStatus>({
method: 'GET',
path: '/api/v1/health/browser',
});
}
// ===== Private Request Handler =====
private async makeRequest<T>(options: {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
path: string;
params?: Record<string, string>;
query?: Record<string, unknown> | object;
body?: unknown;
}): Promise<T> {
const { method, path, params, query, body } = options;
// Build URL
const url = buildFullUrl(this.config.baseUrl, path, params, query);
// Prepare request options
const fetchOptions: RequestInit & { timeout?: number } = {
method,
timeout: this.config.timeout,
};
// Add body for POST/PUT requests
if (body && (method === 'POST' || method === 'PUT')) {
fetchOptions.body = JSON.stringify(body);
}
// Log request if debug enabled
if (this.config.debug) {
logRequest(method, url, body);
}
// Make request with retry logic
return withRetry(async () => {
const response = await fetchWithTimeout(url, fetchOptions);
// Log response if debug enabled
if (this.config.debug) {
logResponse(url, response.status);
}
return parseJsonResponse<T>(response);
}, this.config.retry);
}
}
// ===== Convenience Factory Function =====
/**
* Create a new scraping service client
*/
export function createScrapingServiceClient(config: ScrapingServiceConfig): ScrapingServiceClient {
return new ScrapingServiceClient(config);
}
// ===== Type Exports for Main App =====
export type {
// Core types
NovelDetails,
ChapterData,
NovelSearchResult,
SiteConfiguration,
HealthStatus,
DatabaseInfo,
DatabaseId,
// Health types
HealthOverview,
ProxyHealthStatus,
BrowserHealthStatus,
ServiceStatus,
ProxyProviderBreakdown,
// Request types
SearchNovelsRequest,
ScrapeChapterRequest,
BatchScrapeRequest,
ValidateUrlRequest,
TestSiteConfigRequest,
LatestNovelsRequest,
PopularNovelsRequest,
NovelDetailsRequest,
// Response types
ScrapingServiceResponse,
PaginatedResponse,
} from './types';