src / image_search / providers / DuckDuckGoImageProvider.ts
src / image_search / providers / DuckDuckGoImageProvider.ts
import { HTTPClient } from "../../http/HTTPClient";
import { ProviderError } from "../../models/errors";
import { SearchOptions } from "../../types/search";
import { ImageSearchResult } from "../models";
import { BaseImageProvider } from "./BaseImageProvider";
export const DDG_MAIN_URL = "https://duckduckgo.com/";
export const DDG_IMAGE_SEARCH_URL = "https://duckduckgo.com/i.js";
const VQD_REGEX = /vqd=(["']?)([\w-]+)\1/;
export interface DuckDuckGoImageProviderOptions {
httpClient?: HTTPClient;
httpTimeoutSeconds?: number;
}
/**
* DuckDuckGo Image Search provider implementation.
* Queries DuckDuckGo image search endpoints and normalizes response payloads.
* Requires no API keys.
* Mapped from Python `image_search/providers/duckduckgo.py` DuckDuckGoImageProvider.
*/
export class DuckDuckGoImageProvider extends BaseImageProvider {
public static readonly PROVIDER_NAME = "duckduckgo";
constructor(options: DuckDuckGoImageProviderOptions = {}) {
super({
providerName: DuckDuckGoImageProvider.PROVIDER_NAME,
httpClient: options.httpClient,
httpTimeoutSeconds: options.httpTimeoutSeconds,
});
}
/**
* Execute image search using DuckDuckGo endpoints.
*
* @param query Non-empty search query string.
* @param options Search options (numResults, extra kwargs).
* @returns List of normalized ImageSearchResult objects.
* @throws ProviderError if VQD token extraction or HTTP request fails.
*/
public async searchImages(
query: string,
options: SearchOptions = {}
): Promise<ImageSearchResult[]> {
if (!query || !query.trim()) {
return [];
}
const cleanQuery = query.trim();
const numResults = options.numResults ?? 10;
const { apiKey: _k, numResults: _n, provider: _p, ...extraKwargs } = options;
const vqdToken = await this.fetchVqdToken(cleanQuery);
if (!vqdToken) {
throw new ProviderError("Failed to extract DuckDuckGo vqd verification token.");
}
const params: Record<string, string | number | boolean> = {
l: "wt-wt",
o: "json",
q: cleanQuery,
vqd: vqdToken,
f: ",,,",
p: "1",
...(extraKwargs as Record<string, string | number | boolean>),
};
const headers: Record<string, string> = {
Referer: DDG_MAIN_URL,
Accept: "application/json",
};
let response;
try {
response = await this._httpClient.get(DDG_IMAGE_SEARCH_URL, {
params,
headers,
});
} catch (exc: unknown) {
const err = exc instanceof Error ? exc : new Error(String(exc));
throw new ProviderError(`DuckDuckGo image search HTTP request failed: ${err.message}`);
}
if (response.statusCode !== 200 || !response.data || typeof response.data !== "object") {
throw new ProviderError(
`DuckDuckGo image search failed with status code ${response.statusCode}`
);
}
const data = response.data as Record<string, unknown>;
const rawResults = data.results;
if (!Array.isArray(rawResults)) {
return [];
}
const normalized = rawResults.map((item: unknown) =>
this.normalizeResult((item as Record<string, unknown>) || {})
);
return normalized.slice(0, numResults);
}
/**
* Fetch DuckDuckGo initial search page and extract VQD verification token via regex.
* Mapped from Python `_fetch_vqd_token()`.
*/
public async fetchVqdToken(query: string): Promise<string | null> {
const params = { q: query };
const headers = {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
};
try {
const response = await this._httpClient.get(DDG_MAIN_URL, {
params,
headers,
});
if (response.statusCode === 200 && response.text) {
const match = response.text.match(VQD_REGEX);
if (match && match[2]) {
return match[2];
}
}
} catch {
return null;
}
return null;
}
private normalizeResult(rawItem: Record<string, unknown>): ImageSearchResult {
const imageUrl = String(rawItem.image || "");
const sourceUrl = String(rawItem.url || "");
let sourceDomain: string | undefined = undefined;
if (sourceUrl) {
try {
const parsed = new URL(sourceUrl);
sourceDomain = parsed.hostname;
} catch {
sourceDomain = undefined;
}
}
return {
title: String(rawItem.title || ""),
imageUrl,
sourceUrl,
sourceDomain,
width: typeof rawItem.width === "number" ? rawItem.width : undefined,
height: typeof rawItem.height === "number" ? rawItem.height : undefined,
thumbnailUrl: typeof rawItem.thumbnail === "string" ? rawItem.thumbnail : undefined,
raw: rawItem,
};
}
}
import { HTTPClient } from "../../http/HTTPClient";
import { ProviderError } from "../../models/errors";
import { SearchOptions } from "../../types/search";
import { ImageSearchResult } from "../models";
import { BaseImageProvider } from "./BaseImageProvider";
export const DDG_MAIN_URL = "https://duckduckgo.com/";
export const DDG_IMAGE_SEARCH_URL = "https://duckduckgo.com/i.js";
const VQD_REGEX = /vqd=(["']?)([\w-]+)\1/;
export interface DuckDuckGoImageProviderOptions {
httpClient?: HTTPClient;
httpTimeoutSeconds?: number;
}
/**
* DuckDuckGo Image Search provider implementation.
* Queries DuckDuckGo image search endpoints and normalizes response payloads.
* Requires no API keys.
* Mapped from Python `image_search/providers/duckduckgo.py` DuckDuckGoImageProvider.
*/
export class DuckDuckGoImageProvider extends BaseImageProvider {
public static readonly PROVIDER_NAME = "duckduckgo";
constructor(options: DuckDuckGoImageProviderOptions = {}) {
super({
providerName: DuckDuckGoImageProvider.PROVIDER_NAME,
httpClient: options.httpClient,
httpTimeoutSeconds: options.httpTimeoutSeconds,
});
}
/**
* Execute image search using DuckDuckGo endpoints.
*
* @param query Non-empty search query string.
* @param options Search options (numResults, extra kwargs).
* @returns List of normalized ImageSearchResult objects.
* @throws ProviderError if VQD token extraction or HTTP request fails.
*/
public async searchImages(
query: string,
options: SearchOptions = {}
): Promise<ImageSearchResult[]> {
if (!query || !query.trim()) {
return [];
}
const cleanQuery = query.trim();
const numResults = options.numResults ?? 10;
const { apiKey: _k, numResults: _n, provider: _p, ...extraKwargs } = options;
const vqdToken = await this.fetchVqdToken(cleanQuery);
if (!vqdToken) {
throw new ProviderError("Failed to extract DuckDuckGo vqd verification token.");
}
const params: Record<string, string | number | boolean> = {
l: "wt-wt",
o: "json",
q: cleanQuery,
vqd: vqdToken,
f: ",,,",
p: "1",
...(extraKwargs as Record<string, string | number | boolean>),
};
const headers: Record<string, string> = {
Referer: DDG_MAIN_URL,
Accept: "application/json",
};
let response;
try {
response = await this._httpClient.get(DDG_IMAGE_SEARCH_URL, {
params,
headers,
});
} catch (exc: unknown) {
const err = exc instanceof Error ? exc : new Error(String(exc));
throw new ProviderError(`DuckDuckGo image search HTTP request failed: ${err.message}`);
}
if (response.statusCode !== 200 || !response.data || typeof response.data !== "object") {
throw new ProviderError(
`DuckDuckGo image search failed with status code ${response.statusCode}`
);
}
const data = response.data as Record<string, unknown>;
const rawResults = data.results;
if (!Array.isArray(rawResults)) {
return [];
}
const normalized = rawResults.map((item: unknown) =>
this.normalizeResult((item as Record<string, unknown>) || {})
);
return normalized.slice(0, numResults);
}
/**
* Fetch DuckDuckGo initial search page and extract VQD verification token via regex.
* Mapped from Python `_fetch_vqd_token()`.
*/
public async fetchVqdToken(query: string): Promise<string | null> {
const params = { q: query };
const headers = {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
};
try {
const response = await this._httpClient.get(DDG_MAIN_URL, {
params,
headers,
});
if (response.statusCode === 200 && response.text) {
const match = response.text.match(VQD_REGEX);
if (match && match[2]) {
return match[2];
}
}
} catch {
return null;
}
return null;
}
private normalizeResult(rawItem: Record<string, unknown>): ImageSearchResult {
const imageUrl = String(rawItem.image || "");
const sourceUrl = String(rawItem.url || "");
let sourceDomain: string | undefined = undefined;
if (sourceUrl) {
try {
const parsed = new URL(sourceUrl);
sourceDomain = parsed.hostname;
} catch {
sourceDomain = undefined;
}
}
return {
title: String(rawItem.title || ""),
imageUrl,
sourceUrl,
sourceDomain,
width: typeof rawItem.width === "number" ? rawItem.width : undefined,
height: typeof rawItem.height === "number" ? rawItem.height : undefined,
thumbnailUrl: typeof rawItem.thumbnail === "string" ? rawItem.thumbnail : undefined,
raw: rawItem,
};
}
}