import { IMetadataService } from '../domain/services/IMetadataService'; import { UrlMetadata } from '../domain/value-objects/UrlMetadata'; import { URL } from '../domain/value-objects/URL'; import { Result, ok, err } from '../../../shared/core/Result'; interface CitoidResponse { key?: string; version?: number; itemType?: string; creators?: Array<{ firstName?: string; lastName?: string; creatorType?: string; }>; tags?: string[]; title?: string; date?: string; url?: string; abstractNote?: string; publicationTitle?: string; language?: string; libraryCatalog?: string; accessDate?: string; } interface CitoidErrorResponse { Error: string; } export class CitoidMetadataService implements IMetadataService { private readonly baseUrl = 'https://en.wikipedia.org/api/rest_v1/data/citation/zotero/'; private readonly headers = { accept: 'application/json; charset=utf-8;', }; async fetchMetadata(url: URL): Promise> { try { // URL-encode the target URL const encodedUrl = encodeURIComponent(url.value); const fullUrl = this.baseUrl + encodedUrl; const response = await fetch(fullUrl, { method: 'GET', headers: this.headers, }); if (!response.ok) { return err( new Error( `Citoid API request failed with status ${response.status} and message: ${response.statusText}`, ), ); } const data = (await response.json()) as any; // Check if the response is an error if (data && typeof data === 'object' && 'Error' in data) { const errorResponse = data as CitoidErrorResponse; return err(new Error(`Citoid service error: ${errorResponse.Error}`)); } // Check if it's an array response if (!Array.isArray(data) || data.length === 0) { return err(new Error('No metadata found for the given URL')); } // Use the first result const citoidData = data[0] as CitoidResponse; if (!citoidData || !citoidData.itemType) { return err(new Error('Invalid metadata format from Citoid')); } // Extract author from creators array const author = citoidData.creators && citoidData.creators.length > 0 ? this.formatAuthor(citoidData.creators[0]!) : undefined; // Parse published date const publishedDate = citoidData.date ? this.parseDate(citoidData.date) : undefined; const metadataResult = UrlMetadata.create({ url: url.value, title: citoidData.title, description: citoidData.abstractNote, author, publishedDate, siteName: citoidData.publicationTitle || citoidData.libraryCatalog, type: citoidData.itemType, }); return metadataResult; } catch (error) { return err( new Error( `Failed to fetch metadata from Citoid: ${error instanceof Error ? error.message : 'Unknown error'}`, ), ); } } async isAvailable(): Promise { try { // Test with a simple URL to check if the service is available const testUrl = this.baseUrl + encodeURIComponent('https://example.com'); const response = await fetch(testUrl, { method: 'HEAD', headers: this.headers, }); // Service is available if we get any response (even 4xx errors are fine) return response.status < 500; } catch { return false; } } private formatAuthor(creator: { firstName?: string; lastName?: string; }): string { const { firstName, lastName } = creator; if (firstName && lastName) { return `${firstName} ${lastName}`; } else if (lastName) { return lastName; } else if (firstName) { return firstName; } return ''; } private parseDate(dateString: string): Date | undefined { try { // Try to parse the date string const parsed = new Date(dateString); // Check if the date is valid if (isNaN(parsed.getTime())) { return undefined; } return parsed; } catch { return undefined; } } }