A social knowledge tool for researchers built on ATProto
at main 148 lines 3.8 kB view raw
1import { IMetadataService } from '../domain/services/IMetadataService'; 2import { UrlMetadata } from '../domain/value-objects/UrlMetadata'; 3import { URL } from '../domain/value-objects/URL'; 4import { Result, err } from '../../../shared/core/Result'; 5 6interface IFramelyMeta { 7 title?: string; 8 description?: string; 9 author?: string; 10 author_url?: string; 11 site?: string; 12 canonical?: string; 13 duration?: number; 14 date?: string; 15 medium?: string; 16} 17 18interface IFramelyLinks { 19 thumbnail?: Array<{ 20 href: string; 21 media?: { 22 height?: number; 23 width?: number; 24 }; 25 }>; 26} 27 28interface IFramelyResponse { 29 id?: string; 30 url?: string; 31 meta?: IFramelyMeta; 32 links?: IFramelyLinks; 33 html?: string; 34 error?: string; 35} 36 37export class IFramelyMetadataService implements IMetadataService { 38 private readonly baseUrl = 'https://iframe.ly/api/iframely'; 39 private readonly apiKey: string; 40 41 constructor(apiKey: string) { 42 if (!apiKey || apiKey.trim().length === 0) { 43 throw new Error('Iframely API key is required'); 44 } 45 this.apiKey = apiKey; 46 } 47 48 async fetchMetadata(url: URL): Promise<Result<UrlMetadata>> { 49 try { 50 const encodedUrl = encodeURIComponent(url.value); 51 const fullUrl = `${this.baseUrl}?url=${encodedUrl}&api_key=${this.apiKey}`; 52 53 const response = await fetch(fullUrl, { 54 method: 'GET', 55 headers: { 56 accept: 'application/json', 57 }, 58 }); 59 60 if (!response.ok) { 61 return err( 62 new Error( 63 `Iframely API request failed with status ${response.status} and message: ${response.statusText}`, 64 ), 65 ); 66 } 67 68 const data = (await response.json()) as IFramelyResponse; 69 70 // Check if the response contains an error 71 if (data.error) { 72 return err(new Error(`Iframely service error: ${data.error}`)); 73 } 74 75 // Check if we have meta data 76 if (!data.meta) { 77 return err(new Error('No metadata found for the given URL')); 78 } 79 80 const meta = data.meta; 81 82 // Parse published date 83 const publishedDate = meta.date ? this.parseDate(meta.date) : undefined; 84 85 // Extract image URL from thumbnail links 86 const imageUrl = this.extractImageUrl(data.links); 87 88 const metadataResult = UrlMetadata.create({ 89 url: url.value, 90 title: meta.title, 91 description: meta.description, 92 author: meta.author, 93 publishedDate, 94 siteName: meta.site, 95 imageUrl, 96 type: meta.medium, 97 }); 98 99 return metadataResult; 100 } catch (error) { 101 return err( 102 new Error( 103 `Failed to fetch metadata from Iframely: ${error instanceof Error ? error.message : 'Unknown error'}`, 104 ), 105 ); 106 } 107 } 108 109 async isAvailable(): Promise<boolean> { 110 try { 111 // Test with a simple URL to check if the service is available 112 const testUrl = `${this.baseUrl}?url=${encodeURIComponent('https://example.com')}&api_key=${this.apiKey}`; 113 const response = await fetch(testUrl, { 114 method: 'HEAD', 115 }); 116 117 // Service is available if we get any response (even 4xx errors are fine) 118 return response.status < 500; 119 } catch { 120 return false; 121 } 122 } 123 124 private parseDate(dateString: string): Date | undefined { 125 try { 126 // Try to parse the date string 127 const parsed = new Date(dateString); 128 129 // Check if the date is valid 130 if (isNaN(parsed.getTime())) { 131 return undefined; 132 } 133 134 return parsed; 135 } catch { 136 return undefined; 137 } 138 } 139 140 private extractImageUrl(links?: IFramelyLinks): string | undefined { 141 if (!links || !links.thumbnail || links.thumbnail.length === 0) { 142 return undefined; 143 } 144 145 // Return the first thumbnail URL 146 return links.thumbnail[0]?.href; 147 } 148}