This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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