Repository: oliverschwendener/ueli
Pull request #1419 (2025-04-20 06:30:20)

feat(FaviconProvider): add DuckDuckGo
This PR adds a new favicon provider: DuckDuckGo


Patch:
diff --git a/src/main/Core/ImageGenerator/FaviconProvider/DuckDuckGoFaviconProvider.ts b/src/main/Core/ImageGenerator/FaviconProvider/DuckDuckGoFaviconProvider.ts
new file mode 100644
index 00000000..aa813b2d
--- /dev/null
+++ b/src/main/Core/ImageGenerator/FaviconProvider/DuckDuckGoFaviconProvider.ts
@@ -0,0 +1,7 @@
+import type { FaviconProvider } from "./FaviconProvider";
+
+export class DuckDuckGoFaviconProvider implements FaviconProvider {
+    public getUrl(host: string): string {
+        return `https://icons.duckduckgo.com/ip3/${host}.ico`;
+    }
+}
diff --git a/src/main/Core/ImageGenerator/FaviconProvider/FaviconProvider.ts b/src/main/Core/ImageGenerator/FaviconProvider/FaviconProvider.ts
new file mode 100644
index 00000000..aab749b0
--- /dev/null
+++ b/src/main/Core/ImageGenerator/FaviconProvider/FaviconProvider.ts
@@ -0,0 +1,3 @@
+export interface FaviconProvider {
+    getUrl(host: string, size?: number): string;
+}
diff --git a/src/main/Core/ImageGenerator/FaviconProvider/FaviconeFaviconProvider.ts b/src/main/Core/ImageGenerator/FaviconProvider/FaviconeFaviconProvider.ts
new file mode 100644
index 00000000..7382caca
--- /dev/null
+++ b/src/main/Core/ImageGenerator/FaviconProvider/FaviconeFaviconProvider.ts
@@ -0,0 +1,5 @@
+export class FaviconeFaviconProvider {
+    public getUrl(host: string, size: number = 48): string {
+        return `https://favicone.com/${host}?s=${size}`;
+    }
+}
diff --git a/src/main/Core/ImageGenerator/FaviconProvider/GoogleFaviconProvider.ts b/src/main/Core/ImageGenerator/FaviconProvider/GoogleFaviconProvider.ts
new file mode 100644
index 00000000..d9aa2077
--- /dev/null
+++ b/src/main/Core/ImageGenerator/FaviconProvider/GoogleFaviconProvider.ts
@@ -0,0 +1,7 @@
+import type { FaviconProvider } from "./FaviconProvider";
+
+export class GoogleFaviconProvider implements FaviconProvider {
+    public getUrl(host: string, size: number = 48): string {
+        return `https://www.google.com/s2/favicons?domain=${host}&sz=${size}`;
+    }
+}
diff --git a/src/main/Core/ImageGenerator/FaviconProvider/index.ts b/src/main/Core/ImageGenerator/FaviconProvider/index.ts
new file mode 100644
index 00000000..e83dbefd
--- /dev/null
+++ b/src/main/Core/ImageGenerator/FaviconProvider/index.ts
@@ -0,0 +1,4 @@
+export * from "./DuckDuckGoFaviconProvider";
+export * from "./FaviconeFaviconProvider";
+export * from "./FaviconProvider";
+export * from "./GoogleFaviconProvider";
diff --git a/src/main/Core/ImageGenerator/ImageGeneratorModule.ts b/src/main/Core/ImageGenerator/ImageGeneratorModule.ts
index d7f2b138..fba243e8 100644
--- a/src/main/Core/ImageGenerator/ImageGeneratorModule.ts
+++ b/src/main/Core/ImageGenerator/ImageGeneratorModule.ts
@@ -2,6 +2,7 @@ import type { UeliModuleRegistry } from "@Core/ModuleRegistry";
 import type { OperatingSystem } from "@common/Core";
 import { join } from "path";
 import { CacheFileNameGenerator } from "./CacheFileNameGenerator";
+import { DuckDuckGoFaviconProvider, FaviconeFaviconProvider, GoogleFaviconProvider } from "./FaviconProvider";
 import type { FileIconExtractor } from "./FileIconExtractor";
 import { FileImageGenerator } from "./FileImageGenerator";
 import { GenericFileIconExtractor } from "./GenericFileIconExtractor";
@@ -12,7 +13,14 @@ import { MacOsApplicationIconExtractor, MacOsFolderIconExtractor } from "./macOS
 
 export class ImageGeneratorModule {
     public static async bootstrap(moduleRegistry: UeliModuleRegistry) {
-        moduleRegistry.register("UrlImageGenerator", new UrlImageGenerator(moduleRegistry.get("SettingsManager")));
+        moduleRegistry.register(
+            "UrlImageGenerator",
+            new UrlImageGenerator(moduleRegistry.get("SettingsManager"), {
+                Google: new GoogleFaviconProvider(),
+                DuckDuckGo: new DuckDuckGoFaviconProvider(),
+                Favicone: new FaviconeFaviconProvider(),
+            }),
+        );
 
         const cacheFolderPath = await ImageGeneratorModule.ensureCacheFolderExists(moduleRegistry);
 
diff --git a/src/main/Core/ImageGenerator/UrlImageGenerator.ts b/src/main/Core/ImageGenerator/UrlImageGenerator.ts
index 93b3de64..cf29900d 100644
--- a/src/main/Core/ImageGenerator/UrlImageGenerator.ts
+++ b/src/main/Core/ImageGenerator/UrlImageGenerator.ts
@@ -1,27 +1,28 @@
 import type { SettingsManager } from "@Core/SettingsManager";
 import type { Image } from "@common/Core/Image";
 import type { UrlImageGenerator as UrlImageGeneratorInterface } from "./Contract";
+import type { FaviconProvider } from "./FaviconProvider";
 
 export class UrlImageGenerator implements UrlImageGeneratorInterface {
-    public constructor(private readonly settingsManager: SettingsManager) {}
+    public constructor(
+        private readonly settingsManager: SettingsManager,
+        private readonly faviconProviders: Record<string, FaviconProvider>,
+    ) {}
 
     public getImage(url: string): Image {
         const { host } = new URL(url);
         const size = 48;
 
-        const imageUrls: Record<string, () => string> = {
-            Google: () => `https://www.google.com/s2/favicons?domain=${host}&sz=${size}`,
-            Favicone: () => `https://favicone.com/${host}?s=${size}`,
-        };
+        const providerName = this.getProviderName();
 
-        return {
-            url: Object.keys(imageUrls).includes(this.getFaviconApiProvider())
-                ? imageUrls[this.getFaviconApiProvider()]()
-                : imageUrls["Google"](),
-        };
+        const provider = Object.keys(this.faviconProviders).includes(providerName)
+            ? this.faviconProviders[providerName]
+            : this.faviconProviders["Google"];
+
+        return { url: provider.getUrl(host, size) };
     }
 
-    private getFaviconApiProvider(): string {
+    private getProviderName(): string {
         return this.settingsManager.getValue("imageGenerator.faviconApiProvider", "Google");
     }
 }
diff --git a/src/renderer/Core/Settings/Pages/General/UrlImageGenerator.tsx b/src/renderer/Core/Settings/Pages/General/UrlImageGenerator.tsx
index 5c3c3fb7..5d20e690 100644
--- a/src/renderer/Core/Settings/Pages/General/UrlImageGenerator.tsx
+++ b/src/renderer/Core/Settings/Pages/General/UrlImageGenerator.tsx
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
 export const UrlImageGenerator = () => {
     const { t } = useTranslation("settingsGeneral");
 
-    const faviconApiProviders = ["Google", "Favicone"];
+    const faviconApiProviders = ["Google", "Favicone", "DuckDuckGo"];
 
     const { value: faviconApiProvider, updateValue: setFaviconApiProvider } = useSetting<string>({
         key: "imageGenerator.faviconApiProvider",


Test patch:
diff --git a/src/main/Core/ImageGenerator/UrlImageGenerator.test.ts b/src/main/Core/ImageGenerator/UrlImageGenerator.test.ts
index 9234e8aa..eeb3887c 100644
--- a/src/main/Core/ImageGenerator/UrlImageGenerator.test.ts
+++ b/src/main/Core/ImageGenerator/UrlImageGenerator.test.ts
@@ -1,45 +1,39 @@
 import type { SettingsManager } from "@Core/SettingsManager";
-import type { Image } from "@common/Core/Image";
 import { describe, expect, it, vi } from "vitest";
 import { UrlImageGenerator } from "./UrlImageGenerator";
 
 describe(UrlImageGenerator, () => {
-    it("should generate correct favicon urls using the Google favicon provider", () => {
-        const getValueMock = vi.fn().mockReturnValue("Google");
-        const settingsManager = <SettingsManager>{ getValue: (k, d) => getValueMock(k, d) };
-
-        const urlImageGenerator = new UrlImageGenerator(settingsManager);
-
-        expect(urlImageGenerator.getImage("https://github.com")).toEqual(<Image>{
-            url: "https://www.google.com/s2/favicons?domain=github.com&sz=48",
-        });
-
-        expect(getValueMock).toHaveBeenCalledWith("imageGenerator.faviconApiProvider", "Google");
-    });
-
-    it("should generate correct favicon urls using the Favicone favicon provider", () => {
-        const getValueMock = vi.fn().mockReturnValue("Favicone");
-        const settingsManager = <SettingsManager>{ getValue: (k, d) => getValueMock(k, d) };
-
-        const urlImageGenerator = new UrlImageGenerator(settingsManager);
-
-        expect(urlImageGenerator.getImage("https://github.com")).toEqual(<Image>{
-            url: "https://favicone.com/github.com?s=48",
+    it("should use the configured provider if its found", () => {
+        const getValueMock = vi.fn().mockReturnValue("b");
+
+        const settingsManager = <SettingsManager>{
+            getValue: (k, d, s) => getValueMock(k, d, s),
+        };
+
+        const urlImageGenerator = new UrlImageGenerator(settingsManager, {
+            a: { getUrl: () => "url_a" },
+            b: { getUrl: () => "url_b" },
+            c: { getUrl: () => "url_c" },
+            Google: { getUrl: () => "url_google" },
         });
 
-        expect(getValueMock).toHaveBeenCalledWith("imageGenerator.faviconApiProvider", "Google");
+        expect(urlImageGenerator.getImage("https://example.com")).toEqual({ url: "url_b" });
     });
 
-    it("should use the Google API when the configured provider is invalid", () => {
-        const getValueMock = vi.fn().mockReturnValue("Invalid");
-        const settingsManager = <SettingsManager>{ getValue: (k, d) => getValueMock(k, d) };
+    it("should use the google provider if the configured provider is not found", () => {
+        const getValueMock = vi.fn().mockReturnValue("d");
 
-        const urlImageGenerator = new UrlImageGenerator(settingsManager);
+        const settingsManager = <SettingsManager>{
+            getValue: (k, d, s) => getValueMock(k, d, s),
+        };
 
-        expect(urlImageGenerator.getImage("https://github.com")).toEqual(<Image>{
-            url: "https://www.google.com/s2/favicons?domain=github.com&sz=48",
+        const urlImageGenerator = new UrlImageGenerator(settingsManager, {
+            a: { getUrl: () => "url_a" },
+            b: { getUrl: () => "url_b" },
+            c: { getUrl: () => "url_c" },
+            Google: { getUrl: () => "url_google" },
         });
 
-        expect(getValueMock).toHaveBeenCalledWith("imageGenerator.faviconApiProvider", "Google");
+        expect(urlImageGenerator.getImage("https://example.com")).toEqual({ url: "url_google" });
     });
 });