This repository has no description
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}