LibreY

- privacy respecting meta search engine
git clone git://git.acid.vegas/LibreY.git
Log | Files | Refs | Archive | README | LICENSE

commit 2c36d28a2f957830e42cb782e96ae7e8cb171e78
parent 85ef67c798fe6c8d3b45b81feed780ef81fa5b3d
Author: Ahwx <ahwx@ahwx.org>
Date: Fri, 25 Aug 2023 14:45:49 +0200

refactor(*): everything (merge pull request #18 from davidovski/the_refactor) Major Refactor

Diffstat:
Mapi.php | 56+++++---------------------------------------------------
Mconfig.php.example | 43++++++++++++++++++++-----------------------
Mengines/ahmia/hidden_service.php | 77++++++++++++++++++++++++++++++++---------------------------------------------
Mengines/bittorrent/1337x.php | 58++++++++++++++++++++++++++++++++--------------------------
Mengines/bittorrent/merge.php | 115+++++++++++++++++++++++++------------------------------------------------------
Mengines/bittorrent/nyaa.php | 74++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mengines/bittorrent/rutor.php | 61+++++++++++++++++++++++++++++++++----------------------------
Mengines/bittorrent/sukebei.php | 44+++-----------------------------------------
Mengines/bittorrent/thepiratebay.php | 76+++++++++++++++++++++++++++++++++++++++-------------------------------------
Mengines/bittorrent/torrentgalaxy.php | 61++++++++++++++++++++++++++++++++++---------------------------
Mengines/bittorrent/yts.php | 50++++++++++++++++++++++++++------------------------
Dengines/duckduckgo/text.php | 204-------------------------------------------------------------------------------
Dengines/google/text.php | 226-------------------------------------------------------------------------------
Mengines/invidious/video.php | 113+++++++++++++++++++++++++++++++++++++++----------------------------------------
Aengines/librex/fallback.php | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dengines/librex/text.php | 53-----------------------------------------------------
Mengines/qwant/image.php | 103+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mengines/special/currency.php | 32+++++++++++++++++++-------------
Mengines/special/definition.php | 13++++++++++---
Mengines/special/ip.php | 5+++--
Aengines/special/special.php | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mengines/special/tor.php | 18+++++++++++-------
Mengines/special/user_agent.php | 5+++--
Mengines/special/weather.php | 39++++++++++++++++++++++-----------------
Mengines/special/wikipedia.php | 32+++++++++++++++++++-------------
Aengines/text/duckduckgo.php | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aengines/text/google.php | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aengines/text/text.php | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minstances.php | 2+-
Mmisc/header.php | 7++-----
Amisc/search_engine.php | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmisc/tools.php | 177+++++++++++++++----------------------------------------------------------------
Msearch.php | 132+++++++++++++++++++------------------------------------------------------------
Msettings.php | 103+++++++++++++++++++++++++++++--------------------------------------------------

34 files changed, 1175 insertions(+), 1372 deletions(-)

diff --git a/api.php b/api.php
@@ -1,9 +1,10 @@
 <?php
-    $config = require "config.php";
     require "misc/tools.php";
+    require "misc/search_engine.php";
 
-    if (!isset($_REQUEST["q"]))
-    {
+    $opts = load_opts();
+
+    if (!$opts->query) {
         echo "<p>Example API request: <a href=\"./api.php?q=gentoo&p=2&t=0\">./api.php?q=gentoo&p=2&t=0</a></p>
         <br/>
         <p>\"q\" is the keyword</p>
@@ -16,54 +17,7 @@
         die();
     }
 
-    $query = $_REQUEST["q"];
-    $query_encoded = urlencode($query);
-    $page = isset($_REQUEST["p"]) ? (int) $_REQUEST["p"] : 0;
-    $type = isset($_REQUEST["t"]) ? (int) $_REQUEST["t"] : 0;
-
-    $results = array();
-
-    switch ($type)
-    {
-        case 0:
-            $engine=$config->preferred_engines['text'];
-            if (is_null($engine))
-                $engine = "google";
-            require "engines/$engine/text.php";
-            $results = get_text_results($query, $page);
-            break;
-        case 1:
-            require "engines/qwant/image.php";
-            $results = get_image_results($query_encoded, $page);
-            break;
-        case 2:
-            require "engines/invidious/video.php";
-            $results = get_video_results($query_encoded);
-            break;
-        case 3:
-            if ($config->disable_bittorent_search)
-                $results = array("error" => "disabled");
-            else
-            {
-                require "engines/bittorrent/merge.php";
-                $results = get_merged_torrent_results($query_encoded);
-            }
-            break;
-        case 4:
-            if ($config->disable_hidden_service_search)
-                $results = array("error" => "disabled");
-            else
-            {
-                require "engines/ahmia/hidden_service.php";
-                $results = get_hidden_service_results($query_encoded);
-            }
-            break;
-        default:
-            require "engines/google/text.php";
-            $results = get_text_results($query_encoded, $page);
-            break;
-    }
-
+    $results = fetch_search_results($opts, false);
     header("Content-Type: application/json");
     echo json_encode($results);
 ?>
diff --git a/config.php.example b/config.php.example
@@ -4,13 +4,9 @@
         // e.g.: fr -> https://google.fr/
         "google_domain" => "com",
 
-        // Google results will be in this language
-        "google_language_site" => "",
-        "google_language_results" => "",
-        "google_number_of_results" => 10,
-
-        // You can set a language for results in wikipedia
-        "wikipedia_language" => "en",
+        // Results will be in this language
+        "language" => "",
+        "number_of_results" => 10,
 
         // You can use any Invidious instance here
         "invidious_instance_for_video_results" => "https://invidious.snopyta.org",
@@ -21,7 +17,8 @@
         "disable_hidden_service_search" => false,
 
         // Fallback to another librex instance if google search fails
-        "instance_fallback" => false, // This might generate a 504 Gateway Timeout error, we are looking into this.
+        // This may greatly increase the time it takes to get a result and in some cases results in 504 errors
+        "instance_fallback" => false,
 
         /*
             Preset privacy friendly frontends for users, these can be overwritten by users in the settings
@@ -31,74 +28,74 @@
         "frontends" => array(
             "invidious" => array(
                 "instance_url" => "",
-                "project_url" => "https://docs.invidious.io/instances/", 
+                "project_url" => "https://docs.invidious.io/instances/",
                 "original_name" => "YouTube",
                 "original_url" => "youtube.com"
             ),
             "rimgo" => array(
                 "instance_url" => "",
-                "project_url" => "https://codeberg.org/video-prize-ranch/rimgo#instances", 
+                "project_url" => "https://codeberg.org/video-prize-ranch/rimgo#instances",
                 "original_name" => "Imgur",
                 "original_url" => "imgur.com"
             ),
             "scribe" => array(
                 "instance_url" => "",
-                "project_url" => "https://git.sr.ht/~edwardloveall/scribe/tree/main/docs/instances.md", 
+                "project_url" => "https://git.sr.ht/~edwardloveall/scribe/tree/main/docs/instances.md",
                 "original_name" => "Medium",
                 "original_url" => "medium.com"
             ),
             "gothub" => array(
                 "instance_url" => "",
-                "project_url" => "https://codeberg.org/gothub/gothub#instances", 
+                "project_url" => "https://codeberg.org/gothub/gothub#instances",
                 "original_name" => "GitHub",
                 "original_url" => "github.com"
             ),
             "nitter" => array(
                 "instance_url" => "",
-                "project_url" => "https://github.com/zedeus/nitter/wiki/Instances", 
+                "project_url" => "https://github.com/zedeus/nitter/wiki/Instances",
                 "original_name" => "Twitter",
                 "original_url" => "twitter.com"
             ),
 
             "libreddit" => array(
                 "instance_url" => "",
-                "project_url" => "https://github.com/libreddit/libreddit-instances/blob/master/instances.md", 
+                "project_url" => "https://github.com/libreddit/libreddit-instances/blob/master/instances.md",
                 "original_name" => "Reddit",
                 "original_url" => "reddit.com"
             ),
             "proxitok" => array(
                 "instance_url" => "",
-                "project_url" => "https://github.com/pablouser1/ProxiTok/wiki/Public-instances", 
+                "project_url" => "https://github.com/pablouser1/ProxiTok/wiki/Public-instances",
                 "original_name" => "TikTok",
                 "original_url" => "tiktok.com"
             ),
             "wikiless" => array(
                 "instance_url" => "",
-                "project_url" => "https://github.com/Metastem/wikiless#instances", 
+                "project_url" => "https://github.com/Metastem/wikiless#instances",
                 "original_name" => "Wikipedia",
                 "original_url" => "wikipedia.org"
             ),
             "quetre" => array(
                 "instance_url" => "",
-                "project_url" => "https://github.com/zyachel/quetre#instances", 
+                "project_url" => "https://github.com/zyachel/quetre#instances",
                 "original_name" => "Quora",
                 "original_url" => "quora.com"
             ),
             "libremdb" => array(
                 "instance_url" => "",
-                "project_url" => "https://github.com/zyachel/libremdb#instances", 
+                "project_url" => "https://github.com/zyachel/libremdb#instances",
                 "original_name" => "IMDb",
                 "original_url" => "imdb.com"
             ),
             "breezewiki" => array(
                 "instance_url" => "",
-                "project_url" => "https://docs.breezewiki.com/Links.html", 
+                "project_url" => "https://docs.breezewiki.com/Links.html",
                 "original_name" => "Fandom",
                 "original_url" => "fandom.com"
             ),
             "anonymousoverflow" => array(
                 "instance_url" => "",
-                "project_url" => "https://github.com/httpjamesm/AnonymousOverflow#clearnet-instances", 
+                "project_url" => "https://github.com/httpjamesm/AnonymousOverflow#clearnet-instances",
                 "original_name" => "StackOverflow",
                 "original_url" => "stackoverflow.com"
             ),
@@ -115,10 +112,10 @@
                 "original_url" => "goodreads.com"
             )
         ),
-        
+
 
         "preferred_engines" => array(
-            
+
             /* replace with "text" => "duckduckgo" to use duckduckgo instead
             * (recommended if being ratelimited */
             "text" => "google"
@@ -150,7 +147,7 @@
             CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
             CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
             CURLOPT_MAXREDIRS => 5,
-            CURLOPT_TIMEOUT => 18,
+            CURLOPT_TIMEOUT => 3,
             CURLOPT_VERBOSE => false
         )
     );
diff --git a/engines/ahmia/hidden_service.php b/engines/ahmia/hidden_service.php
@@ -1,53 +1,40 @@
 <?php
-    function get_hidden_service_results($query)
-    {
-        global $config;
+    require "engines/text/text.php";
 
-        $url = "https://ahmia.fi/search/?q=$query";
-        $response = request($url);
-        $xpath = get_xpath($response);
-
-        $results = array();
-
-        foreach($xpath->query("//ol[@class='searchResults']//li[@class='result']") as $result)
-        {
-            $url = "http://" . $xpath->evaluate(".//cite", $result)[0]->textContent;
-            $title = remove_special($xpath->evaluate(".//h4", $result)[0]->textContent);
-            $description = $xpath->evaluate(".//p", $result)[0]->textContent;
-
-            array_push($results,
-                array (
-                    "title" => $title ? htmlspecialchars($title) : "No description provided",
-                    "url" =>  htmlspecialchars($url),
-                    "base_url" => htmlspecialchars(get_base_url($url)),
-                    "description" => htmlspecialchars($description)
-                )
-            );
+    class TorSearch extends EngineRequest {
+        public function get_request_url() {
+            return "https://ahmia.fi/search/?q=" . urlencode($this->query);
         }
 
-        return $results;
-    }
-
-    function print_hidden_service_results($results)
-    {
-        echo "<div class=\"text-result-container\">";
-
-        foreach($results as $result)
-        {
-            $title = $result["title"];
-            $url = $result["url"];
-            $base_url = $result["base_url"];
-            $description = $result["description"];
-
-            echo "<div class=\"text-result-wrapper\">";
-            echo "<a href=\"$url\">";
-            echo "$base_url";
-            echo "<h2>$title</h2>";
-            echo "</a>";
-            echo "<span>$description</span>";
-            echo "</div>";
+        public function get_results() {
+            $response = curl_multi_getcontent($this->ch);
+            $results = array();
+            $xpath = get_xpath($response);
+
+            if (!$xpath)
+                return $results;
+
+            foreach($xpath->query("//ol[@class='searchResults']//li[@class='result']") as $result)
+            {
+                $url = "http://" . $xpath->evaluate(".//cite", $result)[0]->textContent;
+                $title = remove_special($xpath->evaluate(".//h4", $result)[0]->textContent);
+                $description = $xpath->evaluate(".//p", $result)[0]->textContent;
+
+                array_push($results,
+                    array (
+                        "title" => $title ? htmlspecialchars($title) : "No description provided",
+                        "url" =>  htmlspecialchars($url),
+                        "base_url" => htmlspecialchars(get_base_url($url)),
+                        "description" => htmlspecialchars($description)
+                    )
+                );
+            }
+
+            return $results;
         }
 
-        echo "</div>";
+        public static function print_results($results) {
+            TextSearch::print_results($results);
+        }
     }
 ?>
diff --git a/engines/bittorrent/1337x.php b/engines/bittorrent/1337x.php
@@ -1,34 +1,40 @@
 <?php
-    $_1337x_url = "https://1337x.to/search/$query/1/";
+    class _1337xRequest extends EngineRequest {
+        public function get_request_url() {
+            $query = urlencode($this->query);
+            return "https://1337x.to/search/$query/1/";
+        }
 
-    function get_1337x_results($response)
-    {
-        global $config;
-        $xpath = get_xpath($response);
-        $results = array();
+        public function get_results() {
+            $response = curl_multi_getcontent($this->ch);
 
-        foreach($xpath->query("//table/tbody/tr") as $result)
-        {
+            $xpath = get_xpath($response);
+            $results = array();
 
-            $name = $xpath->evaluate(".//td[@class='coll-1 name']/a", $result)[1]->textContent;
-            $magnet = "./engines/bittorrent/get_magnet_1337x.php?url=https://1337x.to" . $xpath->evaluate(".//td[@class='coll-1 name']/a/@href", $result)[1]->textContent;
-            $size_unformatted = explode(" ", $xpath->evaluate(".//td[contains(@class, 'coll-4 size')]", $result)[0]->textContent);
-            $size = $size_unformatted[0] . " " . preg_replace("/[0-9]+/", "", $size_unformatted[1]);
-            $seeders = $xpath->evaluate(".//td[@class='coll-2 seeds']", $result)[0]->textContent;
-            $leechers = $xpath->evaluate(".//td[@class='coll-3 leeches']", $result)[0]->textContent;
+            if (!$xpath)
+                return $results;
 
-            array_push($results, 
-                array (
-                    "name" => htmlspecialchars($name),
-                    "seeders" => (int) $seeders,
-                    "leechers" => (int) $leechers,
-                    "magnet" => htmlspecialchars($magnet),
-                    "size" => htmlspecialchars($size),
-                    "source" => "1337x.to"
-                )
-            );
-        }
+            foreach($xpath->query("//table/tbody/tr") as $result) {
+                $name = $xpath->evaluate(".//td[@class='coll-1 name']/a", $result)[1]->textContent;
+                $magnet = "./engines/bittorrent/get_magnet_1337x.php?url=https://1337x.to" . $xpath->evaluate(".//td[@class='coll-1 name']/a/@href", $result)[1]->textContent;
+                $size_unformatted = explode(" ", $xpath->evaluate(".//td[contains(@class, 'coll-4 size')]", $result)[0]->textContent);
+                $size = $size_unformatted[0] . " " . preg_replace("/[0-9]+/", "", $size_unformatted[1]);
+                $seeders = $xpath->evaluate(".//td[@class='coll-2 seeds']", $result)[0]->textContent;
+                $leechers = $xpath->evaluate(".//td[@class='coll-3 leeches']", $result)[0]->textContent;
 
-        return $results;
+                array_push($results,
+                    array (
+                        "name" => htmlspecialchars($name),
+                        "seeders" => (int) $seeders,
+                        "leechers" => (int) $leechers,
+                        "magnet" => htmlspecialchars($magnet),
+                        "size" => htmlspecialchars($size),
+                        "source" => "1337x.to"
+                    )
+                );
+            }
+
+            return $results;
+        }
     }
 ?>
diff --git a/engines/bittorrent/merge.php b/engines/bittorrent/merge.php
@@ -1,89 +1,48 @@
 <?php
+    class TorrentSearch extends EngineRequest {
+        public function __construct($opts, $mh) {
+            parent::__construct($opts, $mh);
 
-    function get_merged_torrent_results($query)
-    {
-        global $config;
+            require "engines/bittorrent/thepiratebay.php";
+            require "engines/bittorrent/rutor.php";
+            require "engines/bittorrent/yts.php";
+            require "engines/bittorrent/torrentgalaxy.php";
+            require "engines/bittorrent/1337x.php";
+            require "engines/bittorrent/sukebei.php";
 
-        require "engines/bittorrent/thepiratebay.php";
-        require "engines/bittorrent/rutor.php";
-        require "engines/bittorrent/nyaa.php";
-        require "engines/bittorrent/yts.php";
-        require "engines/bittorrent/torrentgalaxy.php";
-        require "engines/bittorrent/1337x.php";
-        require "engines/bittorrent/sukebei.php";
-
-        $query = urlencode($query);
-
-        $torrent_urls = array(
-            $thepiratebay_url,
-            $rutor_url,
-            $nyaa_url,
-            $yts_url,
-            $torrentgalaxy_url,
-            $_1337x_url,
-            $sukebei_url
-        );
- 
-        $mh = curl_multi_init();
-        $chs = $results = array();
-
-        foreach ($torrent_urls as $url)
-        {
-            $ch = curl_init($url);
-            curl_setopt_array($ch, $config->curl_settings);
-            array_push($chs, $ch);
-            curl_multi_add_handle($mh, $ch);    
+            $this->requests = array(
+                new PirateBayRequest($opts, $mh),
+                new _1337xRequest($opts, $mh),
+                new NyaaRequest($opts, $mh),
+                new RutorRequest($opts, $mh),
+                new SukebeiRequest($opts, $mh),
+                new TorrentGalaxyRequest($opts, $mh),
+                new YTSRequest($opts, $mh),
+            );
         }
 
-        $running = null;
-        do {
-            curl_multi_exec($mh, $running);
-        } while ($running);
+        public function get_results() {
+            $results = array();
+            foreach ($this->requests as $request) {
+                if ($request->successful())
+                    $results = array_merge($results, $request->get_results());
+            }
 
-        for ($i=0; count($chs)>$i; $i++)
-        {
-            $response = curl_multi_getcontent($chs[$i]);
+            $seeders = array_column($results, "seeders");
+            array_multisort($seeders, SORT_DESC, $results);
 
-            switch ($i)
-            {
-                case 0:
-                    $results = array_merge($results, get_thepiratebay_results($response));
-                    break;
-                case 1:
-                    $results = array_merge($results, get_rutor_results($response));
-                    break;
-                case 2:
-                    $results = array_merge($results, get_nyaa_results($response));
-                    break;
-                case 3:
-                    $results = array_merge($results, get_yts_results($response));
-                    break;
-                case 4:
-                    $results = array_merge($results, get_torrentgalaxy_results($response));
-                    break;
-                case 5:
-                    $results = array_merge($results, get_1337x_results($response));
-                    break;
-                case 6:
-                    $results = array_merge($results, get_sukebei_results($response));
-                    break;
-            }
+            return $results; 
         }
-        
-        $seeders = array_column($results, "seeders");
-        array_multisort($seeders, SORT_DESC, $results);
 
-        return $results; 
-    }
+        public static function print_results($results) {
+            echo "<div class=\"text-result-container\">";
 
-    function print_merged_torrent_results($results)
-    {
-        echo "<div class=\"text-result-container\">";
+            if (empty($results)) {
+                echo "<p>There are no results. Please try different keywords!</p>";
+                return;
+            }
 
-        if (!empty($results)) 
-        {
-            foreach($results as $result)
-            {
+            foreach($results as $result) {
                 $source = $result["source"];
                 $name = $result["name"];
                 $magnet = $result["magnet"];
@@ -101,11 +60,9 @@
                 echo "$size</span>";
                 echo "</div>";
             }
-        }
-        else
-            echo "<p>There are no results. Please try different keywords!</p>";
 
-        echo "</div>";
+            echo "</div>";
+        }
     }
 
 ?>
diff --git a/engines/bittorrent/nyaa.php b/engines/bittorrent/nyaa.php
@@ -1,35 +1,53 @@
 <?php
-    $nyaa_url = "https://nyaa.si/?q=$query";
+    class NyaaRequest extends EngineRequest {
+        public $SOURCE = "nyaa.si";
 
-    function get_nyaa_results($response)
-    {
-        global $config;
-        $xpath = get_xpath($response);
-        $results = array();
+        public function get_request_url() {
+            return "https://$this->SOURCE/?q=" . urlencode($this->query);
+        }
 
-        foreach($xpath->query("//tbody/tr") as $result)
-        {
-            $name = $xpath->evaluate(".//td[@colspan='2']//a[not(contains(@class, 'comments'))]/@title", $result)[0]->textContent;
-            $centered = $xpath->evaluate(".//td[@class='text-center']", $result);
-            $magnet = $xpath->evaluate(".//a[2]/@href", $centered[0])[0]->textContent;
-            $magnet_without_tracker = explode("&tr=", $magnet)[0];
-            $magnet = $magnet_without_tracker . $config->bittorent_trackers;
-            $size =  $centered[1]->textContent;
-            $seeders =  $centered[3]->textContent;
-            $leechers =  $centered[4]->textContent;
+        public function get_results() {
+            $response = curl_multi_getcontent($this->ch);
+            $xpath = get_xpath($response);
+            $results = array();
 
-            array_push($results, 
-                array (
-                    "name" => htmlspecialchars($name),
-                    "seeders" => (int) $seeders,
-                    "leechers" => (int) $leechers,
-                    "magnet" => htmlspecialchars($magnet),
-                    "size" => htmlspecialchars($size),
-                    "source" => "nyaa.si"
-                )
-            );
-        }
+            if (!$xpath)
+                return $results;
+
+            foreach($xpath->query("//tbody/tr") as $result)
+            {
+                $name_node = $xpath->evaluate(".//td[@colspan='2']//a[not(contains(@class, 'comments'))]/@title", $result);
+                if ($name_node->length > 0) {
+                    $name = $name_node[0]->textContent;
+                } else {
+                    $name = "";
+                }
+                $centered = $xpath->evaluate(".//td[@class='text-center']", $result);
+                $magnet_node = $xpath->evaluate(".//a[2]/@href", $centered[0]);
+                if ($magnet_node->length > 0) {
+                    $magnet = $magnet_node[0]->textContent;
+                    $magnet_without_tracker = explode("&tr=", $magnet)[0];
+                    $magnet = $magnet_without_tracker . $this->opts->bittorent_trackers;
+                } else {
+                    $magnet = "";
+                }
+                $size =  $centered[1]->textContent;
+                $seeders =  $centered[3]->textContent;
+                $leechers =  $centered[4]->textContent;
 
-        return $results;
+                array_push($results,
+                    array (
+                        "name" => htmlspecialchars($name),
+                        "seeders" => (int) $seeders,
+                        "leechers" => (int) $leechers,
+                        "magnet" => htmlspecialchars($magnet),
+                        "size" => htmlspecialchars($size),
+                        "source" => $this->SOURCE
+                    )
+                );
+            }
+
+            return $results;
+        }
     }
 ?>
diff --git a/engines/bittorrent/rutor.php b/engines/bittorrent/rutor.php
@@ -1,36 +1,41 @@
 <?php
-    $rutor_url = "http://rutor.info/search/$query";
+    class RutorRequest extends EngineRequest {
+        public function get_request_url() {
+            return "http://rutor.info/search/" . urlencode($this->query);
+        }
 
-    function get_rutor_results($response)
-    {
-        global $config;
-        $xpath = get_xpath($response);
-        $results = array();
+        public function get_results() {
+            $response = curl_multi_getcontent($this->ch);
+            $xpath = get_xpath($response);
+            $results = array();
 
+            if (!$xpath)
+                return $results;
 
-        foreach($xpath->query("//table/tr[@class='gai' or @class='tum']") as $result)
-        {
-            $name = $xpath->evaluate(".//td/a", $result)[2]->textContent;
-            $magnet =  $xpath->evaluate(".//td/a/@href", $result)[1]->textContent;
-            $magnet_without_tracker = explode("&tr=", $magnet)[0];
-            $magnet = $magnet_without_tracker . $config->bittorent_trackers;
-            $td = $xpath->evaluate(".//td", $result);
-            $size = $td[count($td) == 5 ? 3 : 2]->textContent;
-            $seeders = $xpath->evaluate(".//span", $result)[0]->textContent;
-            $leechers = $xpath->evaluate(".//span", $result)[1]->textContent;
+            foreach($xpath->query("//table/tr[@class='gai' or @class='tum']") as $result)
+            {
+                $name = $xpath->evaluate(".//td/a", $result)[2]->textContent;
+                $magnet =  $xpath->evaluate(".//td/a/@href", $result)[1]->textContent;
+                $magnet_without_tracker = explode("&tr=", $magnet)[0];
+                $magnet = $magnet_without_tracker . $this->opts->bittorrent_trackers;
+                $td = $xpath->evaluate(".//td", $result);
+                $size = $td[count($td) == 5 ? 3 : 2]->textContent;
+                $seeders = $xpath->evaluate(".//span", $result)[0]->textContent;
+                $leechers = $xpath->evaluate(".//span", $result)[1]->textContent;
 
-            array_push($results, 
-                array (
-                    "name" => htmlspecialchars($name),
-                    "seeders" => (int) filter_var($seeders, FILTER_SANITIZE_NUMBER_INT),
-                    "leechers" => (int) filter_var($leechers, FILTER_SANITIZE_NUMBER_INT),
-                    "magnet" => htmlspecialchars($magnet),
-                    "size" => htmlspecialchars($size),
-                    "source" => "rutor.info"
-                )
-            );
-        }
+                array_push($results,
+                    array (
+                        "name" => htmlspecialchars($name),
+                        "seeders" => (int) filter_var($seeders, FILTER_SANITIZE_NUMBER_INT),
+                        "leechers" => (int) filter_var($leechers, FILTER_SANITIZE_NUMBER_INT),
+                        "magnet" => htmlspecialchars($magnet),
+                        "size" => htmlspecialchars($size),
+                        "source" => "rutor.info"
+                    )
+                );
+            }
 
-        return $results;
+            return $results;
+        }
     }
 ?>
diff --git a/engines/bittorrent/sukebei.php b/engines/bittorrent/sukebei.php
@@ -1,44 +1,6 @@
 <?php
-    $sukebei_url = "https://sukebei.nyaa.si/?q=$query";
-
-    function get_sukebei_results($response)
-    {
-        global $config;
-        $xpath = get_xpath($response);
-        $results = array();
-
-        foreach($xpath->query("//tbody/tr") as $result)
-        {
-            $name_node = $xpath->evaluate(".//td[@colspan='2']//a[not(contains(@class, 'comments'))]/@title", $result);
-            if ($name_node->length > 0) {
-                $name = $name_node[0]->textContent;
-            } else {
-                $name = "";
-            }
-            $centered = $xpath->evaluate(".//td[@class='text-center']", $result);
-            $magnet_node = $xpath->evaluate(".//a[2]/@href", $centered[0]);
-            if ($magnet_node->length > 0) {
-                $magnet = $magnet_node[0]->textContent;
-                $magnet_without_tracker = explode("&tr=", $magnet)[0];
-                $magnet = $magnet_without_tracker . $config->bittorent_trackers;
-            } else {
-                $magnet = "";
-            }
-            $size =  $centered[1]->textContent;
-            $seeders =  $centered[3]->textContent;
-            $leechers =  $centered[4]->textContent;
-
-            array_push($results, 
-                array (
-                    "name" => htmlspecialchars($name),
-                    "seeders" => (int) $seeders,
-                    "leechers" => (int) $leechers,
-                    "magnet" => htmlspecialchars($magnet),
-                    "size" => htmlspecialchars($size),
-                    "source" => "sukebei.nyaa.si"
-                )
-            );
-        }
-        return $results;
+    include "engines/bittorrent/nyaa.php";
+    class SukebeiRequest extends NyaaRequest {
+        public $SOURCE = "sukebei.nyaa.si";
     }
 ?>
diff --git a/engines/bittorrent/thepiratebay.php b/engines/bittorrent/thepiratebay.php
@@ -1,44 +1,46 @@
 <?php
-
-    $thepiratebay_url = "https://apibay.org/q.php?q=$query";
-
-    function get_thepiratebay_results($response)
-    {
-        global $config;
-        $results = array();
-        $json_response = json_decode($response, true);
-
-        if (empty($json_response))
-        {
-            return $results;
+    class PirateBayRequest extends EngineRequest {
+        public function get_request_url() {
+            return "https://apibay.org/q.php?q=" . urlencode($this->query);
         }
 
-        foreach ($json_response as $response)
-        {
-            $size = human_filesize($response["size"]);
-            $hash = $response["info_hash"]; 
-            $name = $response["name"];
-            $seeders = (int) $response["seeders"];
-            $leechers = (int) $response["leechers"];
+        public function get_results() {
+            $response = curl_multi_getcontent($this->ch);
+            $results = array();
+            $json_response = json_decode($response, true);
+
+            if (empty($json_response))
+            {
+                return $results;
+            }
+
+            foreach ($json_response as $response)
+            {
+                $size = human_filesize($response["size"]);
+                $hash = $response["info_hash"]; 
+                $name = $response["name"];
+                $seeders = (int) $response["seeders"];
+                $leechers = (int) $response["leechers"];
+
+                $magnet = "magnet:?xt=urn:btih:$hash&dn=$name" . $this->opts->bittorrent_trackers;
+
+                if ($name == "No results returned")
+                    break;
+
+                array_push($results, 
+                    array (
+                        "size" => htmlspecialchars($size),
+                        "name" => htmlspecialchars($name),
+                        "seeders" => (int) htmlspecialchars($seeders),
+                        "leechers" => (int) htmlspecialchars($leechers),
+                        "magnet" => htmlspecialchars($magnet),
+                        "source" => "thepiratebay.org"
+                    )
+                );
+            }
 
-            $magnet = "magnet:?xt=urn:btih:$hash&dn=$name" . $config->bittorent_trackers;
-
-            if ($name == "No results returned")
-                break;
-
-            array_push($results, 
-                array (
-                    "size" => htmlspecialchars($size),
-                    "name" => htmlspecialchars($name),
-                    "seeders" => (int) htmlspecialchars($seeders),
-                    "leechers" => (int) htmlspecialchars($leechers),
-                    "magnet" => htmlspecialchars($magnet),
-                    "source" => "thepiratebay.org"
-                )
-            );
+            return $results;
+           
         }
-
-        return $results;
-       
     }
 ?>
diff --git a/engines/bittorrent/torrentgalaxy.php b/engines/bittorrent/torrentgalaxy.php
@@ -1,34 +1,41 @@
 <?php
-    $torrentgalaxy_url = "https://torrentgalaxy.to/torrents.php?search=$query#results";
+    class TorrentGalaxyRequest extends EngineRequest {
+        public function get_request_url() {
+            $query = urlencode($this->query);
+            return "https://torrentgalaxy.to/torrents.php?search=$query#results";
+        }
 
-    function get_torrentgalaxy_results($response)
-    {
-        global $config;
-        $xpath = get_xpath($response);
-        $results = array();
+        public function get_results() {
+            $response = curl_multi_getcontent($this->ch);
+            $xpath = get_xpath($response);
+            $results = array();
 
-        foreach($xpath->query("//div[@class='tgxtablerow txlight']") as $result)
-        {
-            $name = $xpath->evaluate(".//div[contains(@class, 'clickable-row')]", $result)[0]->textContent;
-            $magnet = $xpath->evaluate(".//div[@class='tgxtablecell collapsehide rounded txlight']/a/@href", $result)[1]->textContent;
-            $magnet_without_tracker = explode("&tr=", $magnet)[0];
-            $magnet = $magnet_without_tracker . $config->bittorent_trackers;
-            $size = $xpath->evaluate(".//div[@class='tgxtablecell collapsehide rounded txlight']/span", $result)[0]->textContent;
-            $seeders = $xpath->evaluate(".//div[@class='tgxtablecell collapsehide rounded txlight']/span/font", $result)[1]->textContent;
-            $leechers = $xpath->evaluate(".//div[@class='tgxtablecell collapsehide rounded txlight']/span/font", $result)[2]->textContent;
+            if (!$xpath)
+                return $results;
 
-            array_push($results, 
-                array (
-                    "name" => htmlspecialchars($name),
-                    "seeders" => (int) $seeders,
-                    "leechers" => (int) $leechers,
-                    "magnet" => htmlspecialchars($magnet),
-                    "size" => htmlspecialchars($size),
-                    "source" => "torrentgalaxy.to"
-                )
-            );
-        }
+            foreach($xpath->query("//div[@class='tgxtablerow txlight']") as $result)
+            {
+                $name = $xpath->evaluate(".//div[contains(@class, 'clickable-row')]", $result)[0]->textContent;
+                $magnet = $xpath->evaluate(".//div[@class='tgxtablecell collapsehide rounded txlight']/a/@href", $result)[1]->textContent;
+                $magnet_without_tracker = explode("&tr=", $magnet)[0];
+                $magnet = $magnet_without_tracker . $this->opts->bittorrent_trackers;
+                $size = $xpath->evaluate(".//div[@class='tgxtablecell collapsehide rounded txlight']/span", $result)[0]->textContent;
+                $seeders = $xpath->evaluate(".//div[@class='tgxtablecell collapsehide rounded txlight']/span/font", $result)[1]->textContent;
+                $leechers = $xpath->evaluate(".//div[@class='tgxtablecell collapsehide rounded txlight']/span/font", $result)[2]->textContent;
 
-        return $results;
+                array_push($results,
+                    array (
+                        "name" => htmlspecialchars($name),
+                        "seeders" => (int) $seeders,
+                        "leechers" => (int) $leechers,
+                        "magnet" => htmlspecialchars($magnet),
+                        "size" => htmlspecialchars($size),
+                        "source" => "torrentgalaxy.to"
+                    )
+                );
+            }
+
+            return $results;
+        }
     }
 ?>
diff --git a/engines/bittorrent/yts.php b/engines/bittorrent/yts.php
@@ -1,14 +1,18 @@
 <?php
-    $yts_url = "https://yts.mx/api/v2/list_movies.json?query_term=$query";
+    class YTSRequest extends EngineRequest {
+        public function get_request_url() {
+            return "https://yts.mx/api/v2/list_movies.json?query_term=" . urlencode($this->query);
+        }
+
+        public function get_results() {
+            $response = curl_multi_getcontent($this->ch);
+            global $config;
+            $results = array();
+            $json_response = json_decode($response, true);
 
-    function get_yts_results($response)
-    {
-        global $config;
-        $results = array();
-        $json_response = json_decode($response, true);
+            if ($json_response["status"] != "ok" || $json_response["data"]["movie_count"] == 0)
+                return $results;
 
-        if ($json_response["status"] == "ok" && $json_response["data"]["movie_count"] != 0)
-        {
             foreach ($json_response["data"]["movies"] as $movie)
             {
                     $name = $movie["title"];
@@ -22,24 +26,22 @@
                         $leechers = $torrent["peers"];
                         $size = $torrent["size"];
 
-                        $magnet = "magnet:?xt=urn:btih:$hash&dn=$name_encoded$config->bittorent_trackers";
-
-                        array_push($results, 
-                        array (
-                            "size" => htmlspecialchars($size),
-                            "name" => htmlspecialchars($name),
-                            "seeders" => htmlspecialchars($seeders),
-                            "leechers" => htmlspecialchars($leechers),
-                            "magnet" => htmlspecialchars($magnet),
-                            "source" => "yts.mx"
-                        )
-                    );
-                    
+                        $magnet = "magnet:?xt=urn:btih:$hash&dn=$name_encoded$this->opts->bittorrent_trackers";
+
+                        array_push($results,
+                            array (
+                                "size" => htmlspecialchars($size),
+                                "name" => htmlspecialchars($name),
+                                "seeders" => htmlspecialchars($seeders),
+                                "leechers" => htmlspecialchars($leechers),
+                                "magnet" => htmlspecialchars($magnet),
+                                "source" => "yts.mx"
+                            )
+                        );
                     }
             }
-        }
 
-        return $results;
-       
+            return $results;
+        }
     }
 ?>
diff --git a/engines/duckduckgo/text.php b/engines/duckduckgo/text.php
@@ -1,204 +0,0 @@
-<?php
-    function get_text_results($query, $page)
-    {
-        global $config;
-
-        $mh = curl_multi_init();
-        $query_encoded = urlencode($query);
-        $results = array();
-
-        // $domain = $config->google_domain;
-        $domain = 'com';
-        $site_language = isset($_COOKIE["google_language_site"]) ? trim(htmlspecialchars($_COOKIE["google_language_site"])) : $config->google_language_site;
-        $results_language = isset($_COOKIE["google_language_results"]) ? trim(htmlspecialchars($_COOKIE["google_language_results"])) : $config->google_language_results;
-        $number_of_results = isset($_COOKIE["google_number_of_results"]) ? trim(htmlspecialchars($_COOKIE["google_number_of_results"])) : $config->google_number_of_results;
-
-        $url = "https://html.duckduckgo.$domain/html/?q=$query_encoded&kd=-1&s=" . 3 * $page;
-        if (3 > strlen($site_language) && 0 < strlen($site_language))
-            $url .= "&hl=$site_language";
-
-        if (3 > strlen($results_language) && 0 < strlen($results_language))
-            $url .= "&lr=lang_$results_language";
-
-        if (3 > strlen($number_of_results) && 0 < strlen($number_of_results))
-            $url .= "&num=$number_of_results";
-
-        if (isset($_COOKIE["safe_search"]))
-            $url .= "&safe=medium";
-
-        $google_ch = curl_init($url);
-        curl_setopt_array($google_ch, $config->curl_settings);
-        curl_multi_add_handle($mh, $google_ch);
-
-        $special_search = $page ? 0 : check_for_special_search($query);
-        $special_ch = null;
-        $url = null;
-        if ($special_search != 0)
-        {
-            switch ($special_search)
-            {
-                case 1:
-                    $url = "https://cdn.moneyconvert.net/api/latest.json";
-                    break;
-                case 2:
-                    $split_query = explode(" ", $query);
-                    $reversed_split_q = array_reverse($split_query);
-                    $word_to_define = $reversed_split_q[1];
-                    $url = "https://api.dictionaryapi.dev/api/v2/entries/en/$word_to_define";
-                    break;
-                case 5:
-                    $url = "https://wttr.in/@" . $_SERVER["REMOTE_ADDR"] . "?format=j1";
-                    break;
-                case 6:
-                    $url = "https://check.torproject.org/torbulkexitlist";
-                    break;
-                case 7:
-                    $wikipedia_language = isset($_COOKIE["wikipedia_language"]) ? trim(htmlspecialchars($_COOKIE["wikipedia_language"])) : $config->wikipedia_language;
-                    if (in_array($wikipedia_language, json_decode(file_get_contents("static/misc/wikipedia_langs.json"), true)))
-                        $url = "https://$wikipedia_language.wikipedia.org/w/api.php?format=json&action=query&prop=extracts%7Cpageimages&exintro&explaintext&redirects=1&pithumbsize=500&titles=$query_encoded";
-                    break;
-            }
-            
-            if ($url != NULL)
-            {
-                $special_ch = curl_init($url);
-                curl_setopt_array($special_ch, $config->curl_settings);
-                curl_multi_add_handle($mh, $special_ch);
-            }
-        }
-
-        $running = null;
-        do {
-            curl_multi_exec($mh, $running);
-        } while ($running);
-
-        if (curl_getinfo($google_ch)['http_code'] != '200') 
-        {
-            require "engines/librex/text.php";
-            return get_librex_results($query, $page);
-        }
-
-
-
-        if ($special_search != 0)
-        {
-            $special_result = null;
-
-            switch ($special_search)
-            {
-                case 1:
-                    require "engines/special/currency.php";
-                    $special_result = currency_results($query, curl_multi_getcontent($special_ch));
-                    break;
-                case 2:
-                    require "engines/special/definition.php";
-                    $special_result = definition_results($query, curl_multi_getcontent($special_ch));
-                    break;
-
-                case 3:
-                    require "engines/special/ip.php";
-                    $special_result = ip_result();
-                    break;
-                case 4:
-                    require "engines/special/user_agent.php";
-                    $special_result = user_agent_result();
-                    break;
-                case 5:
-                    require "engines/special/weather.php";
-                    $special_result = weather_results(curl_multi_getcontent($special_ch));
-                    break;
-                case 6:
-                    require "engines/special/tor.php";
-                    $special_result = tor_result(curl_multi_getcontent($special_ch));
-                    break;
-                case 7:
-                    require "engines/special/wikipedia.php";
-                    $special_result = wikipedia_results($query, curl_multi_getcontent($special_ch));
-                    break;
-            }
-
-            if ($special_result != null)
-                array_push($results, $special_result);
-        }
-
-        $xpath = get_xpath(curl_multi_getcontent($google_ch));
-		
-		foreach($xpath->query("/html/body/div[1]/div[". count($xpath->query('/html/body/div[1]/div')) ."]/div/div/div/div") as $result)
-		{
-            $url = $xpath->evaluate(".//h2[@class='result__title']//a/@href", $result)[0];
-			
-			if ($url == null)
-                continue;
-
-            if (!empty($results)) // filter duplicate results, ignore special result
-            {
-                if (!array_key_exists("special_response", end($results)))
-                    if (end($results)["url"] == $url->textContent)
-                        continue;
-            }
-
-            $url = $url->textContent;
-
-            $url = check_for_privacy_frontend($url);
-
-            $title = $xpath->evaluate(".//h2[@class='result__title']", $result)[0];
-            $description = $xpath->evaluate(".//a[@class='result__snippet']", $result)[0];
-
-            array_push($results,
-                array (
-                    "title" => htmlspecialchars($title->textContent),
-                    "url" =>  htmlspecialchars($url),
-                    "base_url" => htmlspecialchars(get_base_url($url)),
-                    "description" =>  $description == null ?
-                                      "No description was provided for this site." :
-                                      htmlspecialchars($description->textContent)
-                )
-            );
-       }
-
-        return $results;
-    }
-
-    function print_text_results($results)
-	{
-        $special = $results[0];
-        if (array_key_exists("special_response", $special))
-        {
-            $response = $special["special_response"]["response"];
-            $source = $special["special_response"]["source"];
-
-            echo "<p class=\"special-result-container\">";
-            if (array_key_exists("image", $special["special_response"]))
-            {
-                $image_url = $special["special_response"]["image"];
-                echo "<img src=\"image_proxy.php?url=$image_url\">";
-            }
-            echo $response;
-            if ($source)
-                echo "<a href=\"$source\" target=\"_blank\">$source</a>";
-            echo "</p>";
-
-            array_shift($results);
-        }
-
-        echo "<div class=\"text-result-container\">";
-
-        foreach($results as $result)
-        {
-            $title = $result["title"];
-            $url = $result["url"];
-            $base_url = $result["base_url"];
-            $description = $result["description"];
-
-            echo "<div class=\"text-result-wrapper\">";
-            echo "<a href=\"$url\">";
-            echo "$base_url";
-            echo "<h2>$title</h2>";
-            echo "</a>";
-            echo "<span>$description</span>";
-            echo "</div>";
-        }
-
-        echo "</div>";
-    }
-?>
diff --git a/engines/google/text.php b/engines/google/text.php
@@ -1,226 +0,0 @@
-<?php
-    function get_text_results($query, $page)
-    {
-        global $config;
-
-        $mh = curl_multi_init();
-        $query_encoded = str_replace("%22", "\"", urlencode($query));
-        $results = array();
-
-        $domain = $config->google_domain;
-        $site_language = isset($_COOKIE["google_language_site"]) ? trim(htmlspecialchars($_COOKIE["google_language_site"])) : $config->google_language_site;
-        $results_language = isset($_COOKIE["google_language_results"]) ? trim(htmlspecialchars($_COOKIE["google_language_results"])) : $config->google_language_results;
-        $number_of_results = isset($_COOKIE["google_number_of_results"]) ? trim(htmlspecialchars($_COOKIE["google_number_of_results"])) : $config->google_number_of_results;
-
-        $url = "https://www.google.$domain/search?q=$query_encoded&nfpr=1&start=$page";
-        error_log($url);
-
-        if (3 > strlen($site_language) && 0 < strlen($site_language))
-            $url .= "&hl=$site_language";
-
-        if (3 > strlen($results_language) && 0 < strlen($results_language))
-            $url .= "&lr=lang_$results_language";
-
-        if (3 > strlen($number_of_results) && 0 < strlen($number_of_results))
-            $url .= "&num=$number_of_results";
-
-        if (isset($_COOKIE["safe_search"]))
-            $url .= "&safe=medium";
-
-        $google_ch = curl_init($url);
-        curl_setopt_array($google_ch, $config->curl_settings);
-        curl_multi_add_handle($mh, $google_ch);
-
-        $special_search = $page ? 0 : check_for_special_search($query);
-        $special_ch = null;
-        $url = null;
-        if ($special_search != 0)
-        {
-            switch ($special_search)
-            {
-                case 1:
-                    $url = "https://cdn.moneyconvert.net/api/latest.json";
-                    break;
-                case 2:
-                    $split_query = explode(" ", $query);
-                    $reversed_split_q = array_reverse($split_query);
-                    $word_to_define = $reversed_split_q[1];
-                    $url = "https://api.dictionaryapi.dev/api/v2/entries/en/$word_to_define";
-                    break;
-                case 5:
-                    $url = "https://wttr.in/@" . $_SERVER["REMOTE_ADDR"] . "?format=j1";
-                    break;
-                case 6:
-                    $url = "https://check.torproject.org/torbulkexitlist";
-                    break;
-                case 7:
-                    $wikipedia_language = isset($_COOKIE["wikipedia_language"]) ? trim(htmlspecialchars($_COOKIE["wikipedia_language"])) : $config->wikipedia_language;
-                    if (in_array($wikipedia_language, json_decode(file_get_contents("static/misc/wikipedia_langs.json"), true)))
-                        $url = "https://$wikipedia_language.wikipedia.org/w/api.php?format=json&action=query&prop=extracts%7Cpageimages&exintro&explaintext&redirects=1&pithumbsize=500&titles=$query_encoded";
-                    break;
-            }
-
-            if ($url != NULL)
-            {
-                $special_ch = curl_init($url);
-                curl_setopt_array($special_ch, $config->curl_settings);
-                curl_multi_add_handle($mh, $special_ch);
-            }
-        }
-
-        $running = null;
-        do {
-            curl_multi_exec($mh, $running);
-        } while ($running);
-
-        if (curl_getinfo($google_ch)['http_code'] != '200') 
-        {
-            require "engines/librex/text.php";
-            return get_librex_results($query, $page);
-        }
-
-
-        $special_result = array();
-        if ($special_search != 0)
-        {
-
-            switch ($special_search)
-            {
-                case 1:
-                    require "engines/special/currency.php";
-                    $special_result = currency_results($query, curl_multi_getcontent($special_ch));
-                    break;
-                case 2:
-                    require "engines/special/definition.php";
-                    $special_result = definition_results($query, curl_multi_getcontent($special_ch));
-                    break;
-
-                case 3:
-                    require "engines/special/ip.php";
-                    $special_result = ip_result();
-                    break;
-                case 4:
-                    require "engines/special/user_agent.php";
-                    $special_result = user_agent_result();
-                    break;
-                case 5:
-                    require "engines/special/weather.php";
-                    $special_result = weather_results(curl_multi_getcontent($special_ch));
-                    break;
-                case 6:
-                    require "engines/special/tor.php";
-                    $special_result = tor_result(curl_multi_getcontent($special_ch));
-                    break;
-                case 7:
-                    require "engines/special/wikipedia.php";
-                    $special_result = wikipedia_results($query, curl_multi_getcontent($special_ch));
-                    break;
-            }
-        }
-
-        $xpath = get_xpath(curl_multi_getcontent($google_ch));
-
-        $didyoumean = $xpath->query(".//a[@class='gL9Hy']")[0];
-
-        if (!is_null($didyoumean))
-            $special_result["did_you_mean"] = $didyoumean->textContent;
-
-        if (!empty($special_result))
-            array_push($results, $special_result);
-
-
-
-        foreach($xpath->query("//div[@id='search']//div[contains(@class, 'g')]") as $result)
-        {
-            $url = $xpath->evaluate(".//div[@class='yuRUbf']//a/@href", $result)[0];
-
-            if ($url == null)
-                continue;
-
-            if (!empty($results)) // filter duplicate results, ignore special result
-            {
-                if (!array_key_exists("special_response", end($results)))
-                    if (end($results)["url"] == $url->textContent)
-                        continue;
-            }
-
-            $url = $url->textContent;
-
-            $url = check_for_privacy_frontend($url);
-
-            $title = $xpath->evaluate(".//h3", $result)[0];
-            $description = $xpath->evaluate(".//div[contains(@class, 'VwiC3b')]", $result)[0];
-
-            array_push($results,
-                array (
-                    "title" => htmlspecialchars($title->textContent),
-                    "url" =>  htmlspecialchars($url),
-                    "base_url" => htmlspecialchars(get_base_url($url)),
-                    "description" =>  $description == null ?
-                                      "No description was provided for this site." :
-                                      htmlspecialchars($description->textContent)
-                )
-            );
-        }
-
-        return $results;
-    }
-
-    function print_text_results($results)
-    {
-
-        if (empty($results))
-            return;
-
-        $special = $results[0];
-
-        if (array_key_exists("did_you_mean", $special)) 
-        {
-            $didyoumean = $special["did_you_mean"];
-            $new_url = "/search.php?q="  . urlencode($didyoumean);
-            echo "<p class=\"did-you-mean\">Did you mean ";
-            echo "<a href=\"$new_url\">$didyoumean</a>";
-            echo "?</p>";
-        }
-
-        if (array_key_exists("special_response", $special)) 
-        {
-            $response = $special["special_response"]["response"];
-            $source = $special["special_response"]["source"];
-
-            echo "<p class=\"special-result-container\">";
-            if (array_key_exists("image", $special["special_response"]))
-            {
-                $image_url = $special["special_response"]["image"];
-                echo "<img src=\"image_proxy.php?url=$image_url\">";
-            }
-            echo $response;
-            if ($source)
-                echo "<a href=\"$source\" target=\"_blank\">$source</a>";
-            echo "</p>";
-        }
-
-        echo "<div class=\"text-result-container\">";
-
-        foreach($results as $result)
-        {
-            if (!array_key_exists("title", $result))
-                continue;
-
-            $title = $result["title"];
-            $url = $result["url"];
-            $base_url = $result["base_url"];
-            $description = $result["description"];
-
-            echo "<div class=\"text-result-wrapper\">";
-            echo "<a href=\"$url\">";
-            echo "$base_url";
-            echo "<h2>$title</h2>";
-            echo "</a>";
-            echo "<span>$description</span>";
-            echo "</div>";
-        }
-
-        echo "</div>";
-    }
-?>
diff --git a/engines/invidious/video.php b/engines/invidious/video.php
@@ -1,68 +1,67 @@
 <?php
-    function get_video_results($query)
-    {
-        global $config;
-        $instance_url = $config->invidious_instance_for_video_results;
-        
-        $url = "$instance_url/api/v1/search?q=$query";
-        $response = request($url);
-        $json_response = json_decode($response, true);
-        $results = array();
+    class VideoSearch extends EngineRequest {
+        public function get_request_url() {
+            $this->instance_url = $this->opts->invidious_instance_for_video_results;
+            $query = urlencode($this->query);
+            return "$this->instance_url/api/v1/search?q=$query";
+        }
+
+        public function get_results() {
+            $results = array();
+            $response = curl_multi_getcontent($this->ch);
+            $json_response = json_decode($response, true);
 
-        foreach ($json_response as $response)
-        {
-            if ($response["type"] == "video")
-            {
-                $title = $response["title"];
-                $url = "https://youtube.com/watch?v=" . $response["videoId"];
-                $url = check_for_privacy_frontend($url);
-                $uploader = $response["author"];
-                $views = $response["viewCount"];
-                $date = $response["publishedText"];
-                $thumbnail = $instance_url . "/vi/" . explode("/vi/" ,$response["videoThumbnails"][4]["url"])[1];
+            foreach ($json_response as $response) {
+                if ($response["type"] == "video") {
+                    $title = $response["title"];
+                    $url = "https://youtube.com/watch?v=" . $response["videoId"];
+                    $url = check_for_privacy_frontend($url, $this->opts);
+                    $uploader = $response["author"];
+                    $views = $response["viewCount"];
+                    $date = $response["publishedText"];
+                    $thumbnail = $this->instance_url . "/vi/" . explode("/vi/" ,$response["videoThumbnails"][4]["url"])[1];
 
-                array_push($results,
-                    array (
-                        "title" => htmlspecialchars($title),
-                        "url" =>  htmlspecialchars($url),
-                        "base_url" => htmlspecialchars(get_base_url($url)),
-                        "uploader" => htmlspecialchars($uploader),
-                        "views" => htmlspecialchars($views),
-                        "date" => htmlspecialchars($date),
-                        "thumbnail" => htmlspecialchars($thumbnail)
-                    )
-                );
+                    array_push($results,
+                        array (
+                            "title" => htmlspecialchars($title),
+                            "url" =>  htmlspecialchars($url),
+                            "base_url" => htmlspecialchars(get_base_url($url)),
+                            "uploader" => htmlspecialchars($uploader),
+                            "views" => htmlspecialchars($views),
+                            "date" => htmlspecialchars($date),
+                            "thumbnail" => htmlspecialchars($thumbnail)
+                        )
+                    );
+                }
             }
-        }
 
-        return $results;
-    }
+            return $results;
+        }
 
-    function print_video_results($results)
-    {
-        echo "<div class=\"text-result-container\">";
+        public static function print_results($results) {
+            echo "<div class=\"text-result-container\">";
 
-            foreach($results as $result)
-            {
-                $title = $result["title"];
-                $url = $result["url"];
-                $base_url = $result["base_url"];
-                $uploader = $result["uploader"];
-                $views = $result["views"];
-                $date = $result["date"];
-                $thumbnail = $result["thumbnail"];
+                foreach($results as $result) {
+                    $title = $result["title"];
+                    $url = $result["url"];
+                    $base_url = $result["base_url"];
+                    $uploader = $result["uploader"];
+                    $views = $result["views"];
+                    $date = $result["date"];
+                    $thumbnail = $result["thumbnail"];
 
-                echo "<div class=\"text-result-wrapper\">";
-                echo "<a href=\"$url\">";
-                echo "$base_url";
-                echo "<h2>$title</h2>";
-                echo "<img class=\"video-img\" src=\"image_proxy.php?url=$thumbnail\">";
-                echo "<br>";
-                echo "<span>$uploader - $date - $views views</span>";
-                echo "</a>";
-                echo "</div>";
-            }
+                    echo "<div class=\"text-result-wrapper\">";
+                    echo "<a href=\"$url\">";
+                    echo "$base_url";
+                    echo "<h2>$title</h2>";
+                    echo "<img class=\"video-img\" src=\"image_proxy.php?url=$thumbnail\">";
+                    echo "<br>";
+                    echo "<span>$uploader - $date - $views views</span>";
+                    echo "</a>";
+                    echo "</div>";
+                }
 
-        echo "</div>";
+            echo "</div>";
+        }
     }
 ?>
diff --git a/engines/librex/fallback.php b/engines/librex/fallback.php
@@ -0,0 +1,63 @@
+<?php
+
+    class LibreXFallback extends EngineRequest {
+        public function __construct($instance, $opts, $mh) {
+            $this->instance = $instance;
+            parent::__construct($opts, $mh);
+        }
+
+        public function get_request_url() {
+           return $this->instance . "api.php?" . opts_to_params($this->opts);
+        }
+
+        public function get_results() {
+            $response = curl_exec($this->ch);
+            $response = json_decode($response, true);
+            if (!$response)
+                return array();
+            
+            return array_values($response);
+        }
+    }
+
+
+    function get_librex_results($opts) {
+        if (!$opts->do_fallback)
+            return array();
+
+        $instances_json = json_decode(file_get_contents("instances.json"), true);
+
+        if (empty($instances_json["instances"]))
+            return array();
+
+        // TODO pick instances which aren't on cooldown
+
+        $instances = array_map(fn($n) => $n['clearnet'], array_filter($instances_json['instances'], fn($n) => !is_null($n['clearnet'])));
+        shuffle($instances);
+
+        $results = array();
+        $tries = 0;
+
+        do {
+            $tries++;
+
+            $instance = array_pop($instances);
+
+            if (parse_url($instance)["host"] == parse_url($_SERVER['HTTP_HOST'])["host"])
+                continue;
+
+            $librex_request = new LibreXFallback($instance, $opts, null);
+            $results = $librex_request->get_results();
+
+            if (count($results) > 1)
+                return $results;
+
+        } while ( !empty($instances));
+
+        if (empty($instances))
+            return array();
+
+        return array_values($results);
+    }
+
+?>
diff --git a/engines/librex/text.php b/engines/librex/text.php
@@ -1,53 +0,0 @@
-<?php
-
-    function get_librex_results($query, $page) 
-    {
-        global $config;
-
-        if (isset($_REQUEST["nfb"]) && $_REQUEST["nfb"] == "1")
-            return array();
-
-        if (!$config->instance_fallback) 
-            return array();
-
-        $instances_json = json_decode(file_get_contents("instances.json"), true);
-
-        if (empty($instances_json["instances"]))
-            return array();
-
-
-        $instances = array_map(fn($n) => $n['clearnet'], array_filter($instances_json['instances'], fn($n) => !is_null($n['clearnet'])));
-        shuffle($instances);
-
-        $query_encoded = urlencode($query);
-
-        $results = array();
-        $tries = 0;
-
-        do {
-            $tries++;
-
-            $instance = array_pop($instances);
-
-            if (parse_url($instance)["host"] == parse_url($_SERVER['HTTP_HOST'])["host"])
-                continue;
-
-            $url = $instance . "api.php?q=$query_encoded&p=$page&t=0&nfb=1";
-
-            $librex_ch = curl_init($url);
-            curl_setopt_array($librex_ch, $config->curl_settings);
-            copy_cookies($librex_ch);
-            $response = curl_exec($librex_ch);
-            curl_close($librex_ch);
-
-            $code = curl_getinfo($librex_ch)["http_code"];
-            $results = json_decode($response, true);
-
-        } while ( !empty($instances) && ($results == null || count($results) <= 1));
-
-        if (empty($instances))
-            return array();
-
-        return array_values($results);
-    }
-?>
diff --git a/engines/qwant/image.php b/engines/qwant/image.php
@@ -1,60 +1,63 @@
 <?php
-    function get_image_results($query, $page) 
-    {
-        global $config;
+    class QwantImageSearch extends EngineRequest {
+        public function get_request_url() {
+            $page = $this->page / 10 + 1; // qwant has a different page system
+            $query = urlencode($this->query);
+            
+            return "https://lite.qwant.com/?q=$query&t=images&p=$page";
+        }
 
-        $page = $page / 10 + 1; // qwant has a different page system
+        public function get_results() {
+            $results = array();
+            $xpath = get_xpath(curl_multi_getcontent($this->ch));
+
+            if (!$xpath)
+                return $results;
+
+            foreach($xpath->query("//a[@rel='noopener']") as $result)
+            {       
+                    $image = $xpath->evaluate(".//img", $result)[0];
+
+                    if ($image)
+                    {
+                        $encoded_url = $result->getAttribute("href");
+                        $encoded_url_split1 = explode("==/", $encoded_url)[1];
+                        $encoded_url_split2 = explode("?position", $encoded_url_split1)[0];
+                        $real_url = urldecode(base64_decode($encoded_url_split2));
+                        $real_url = check_for_privacy_frontend($real_url, $this->opts);
+
+                        $alt = $image->getAttribute("alt");
+                        $thumbnail = urlencode($image->getAttribute("src"));
+
+                        array_push($results, 
+                            array (
+                                "thumbnail" => urldecode(htmlspecialchars($thumbnail)),
+                                "alt" => htmlspecialchars($alt),
+                                "url" => htmlspecialchars($real_url)
+                            )
+                        );
         
-        $url = "https://lite.qwant.com/?q=$query&t=images&p=$page";
-        $response = request($url);
-        $xpath = get_xpath($response);
-
-        $results = array();
-
-        foreach($xpath->query("//a[@rel='noopener']") as $result)
-        {       
-                $image = $xpath->evaluate(".//img", $result)[0];
+                    }
+            }
 
-                if ($image)
-                {
-                    $encoded_url = $result->getAttribute("href");
-                    $encoded_url_split1 = explode("==/", $encoded_url)[1];
-                    $encoded_url_split2 = explode("?position", $encoded_url_split1)[0];
-                    $real_url = urldecode(base64_decode($encoded_url_split2));
-                    $real_url = check_for_privacy_frontend($real_url);
-
-                    $alt = $image->getAttribute("alt");
-                    $thumbnail = urlencode($image->getAttribute("src"));
-
-                    array_push($results, 
-                        array (
-                            "thumbnail" => urldecode(htmlspecialchars($thumbnail)),
-                            "alt" => htmlspecialchars($alt),
-                            "url" => htmlspecialchars($real_url)
-                        )
-                    );
-    
-                }
+            return $results;
         }
+        
+        public static function print_results($results) {
+            echo "<div class=\"image-result-container\">";
 
-        return $results;
-    }
-
-    function print_image_results($results)
-    {
-        echo "<div class=\"image-result-container\">";
-
-            foreach($results as $result)
-            {
-                $thumbnail = urlencode($result["thumbnail"]);
-                $alt = $result["alt"];
-                $url = $result["url"];
+                foreach($results as $result)
+                {
+                    $thumbnail = urlencode($result["thumbnail"]);
+                    $alt = $result["alt"];
+                    $url = $result["url"];
 
-                echo "<a title=\"$alt\" href=\"$url\" target=\"_blank\">";
-                echo "<img src=\"image_proxy.php?url=$thumbnail\">";
-                echo "</a>";
-            }
+                    echo "<a title=\"$alt\" href=\"$url\" target=\"_blank\">";
+                    echo "<img src=\"image_proxy.php?url=$thumbnail\">";
+                    echo "</a>";
+                }
 
-        echo "</div>";
+            echo "</div>";
+        }
     }
 ?>
diff --git a/engines/special/currency.php b/engines/special/currency.php
@@ -1,18 +1,24 @@
 <?php
-    function currency_results($query, $response)
-    { 
-        $split_query = explode(" ", $query);
-
-        $base_currency = strtoupper($split_query[1]);
-        $currency_to_convert = strtoupper($split_query[3]);
-        $amount_to_convert = floatval($split_query[0]);   
+    class CurrencyRequest extends EngineRequest {
+        public function get_request_url() {
+            return "https://cdn.moneyconvert.net/api/latest.json";
+        }
         
-        $json_response = json_decode($response, true);
-                
-        $rates =  $json_response["rates"];
+        public function get_results() { 
+            $response = curl_multi_getcontent($this->ch);
+
+            $split_query = explode(" ", $this->query);
+
+            $base_currency = strtoupper($split_query[1]);
+            $currency_to_convert = strtoupper($split_query[3]);
+            $amount_to_convert = floatval($split_query[0]);   
+            
+            $json_response = json_decode($response, true);
+                    
+            $rates =  $json_response["rates"];
 
-        if (array_key_exists($base_currency, $rates) && array_key_exists($currency_to_convert, $rates))
-        {
+            if (!array_key_exists($base_currency, $rates) || !array_key_exists($currency_to_convert, $rates))
+                return array();
             $base_currency_response = $rates[$base_currency];
             $currency_to_convert_response = $rates[$currency_to_convert];
 
@@ -26,6 +32,6 @@
                     "source" => $source
                 )
             );
-        }                    
+        }
     }
 ?>
diff --git a/engines/special/definition.php b/engines/special/definition.php
@@ -1,9 +1,15 @@
 <?php
-    function definition_results($query, $response) 
-    {        
-            $split_query = explode(" ", $query);
+    class DefinitionRequest extends EngineRequest {
+
+        public function get_request_url() {
+            $split_query = explode(" ", $this->query);
             $reversed_split_q = array_reverse($split_query);
             $word_to_define = $reversed_split_q[1];
+            return "https://api.dictionaryapi.dev/api/v2/entries/en/$word_to_define";
+        }
+        
+        public function get_results() {        
+            $response = curl_multi_getcontent($this->ch);
 
             $json_response = json_decode($response, true);
 
@@ -20,5 +26,6 @@
                 );
             }
         
+        }
     }
 ?>
diff --git a/engines/special/ip.php b/engines/special/ip.php
@@ -1,11 +1,12 @@
 <?php
-    function ip_result()
-    {
+    class IPRequest extends EngineRequest {
+        function get_results() {
             return array(
                 "special_response" => array(
                     "response" => $_SERVER["REMOTE_ADDR"],
                     "source" => null
                 )
             );
+        }
     }
 ?>
diff --git a/engines/special/special.php b/engines/special/special.php
@@ -0,0 +1,91 @@
+<?php
+
+    function check_for_special_search($query) {
+        if (isset($_COOKIE["disable_special"]))
+            return 0;
+
+         $query_lower = strtolower($query);
+         $split_query = explode(" ", $query);
+
+         if (strpos($query_lower, "to") && count($split_query) >= 4) // currency
+         {
+            $amount_to_convert = floatval($split_query[0]);
+            if ($amount_to_convert != 0)
+                return 1;
+         }
+         else if (strpos($query_lower, "mean") && count($split_query) >= 2) // definition
+         {
+             return 2;
+         }
+         else if (strpos($query_lower, "my") !== false)
+         {
+            if (strpos($query_lower, "ip"))
+            {
+                return 3;
+            }
+            else if (strpos($query_lower, "user agent") || strpos($query_lower, "ua"))
+            {
+                return 4;
+            }
+         }
+         else if (strpos($query_lower, "weather") !== false)
+         {
+                return 5;
+         }
+         else if ($query_lower == "tor")
+         {
+                return 6;
+         }
+         else if (3 > count(explode(" ", $query))) // wikipedia
+         {
+             return 7;
+         }
+
+        return 0;
+    }
+
+    function get_special_search_request($opts, $mh) {
+        if ($opts->page != 0)
+            return null;
+
+        $special_search = check_for_special_search($opts->query);
+        $special_request = null;
+        $url = null;
+
+        if ($special_search == 0)
+            return null;
+
+        switch ($special_search) {
+            case 1:
+                require "engines/special/currency.php";
+                $special_request = new CurrencyRequest($opts, $mh);
+                break;
+            case 2:
+                require "engines/special/definition.php";
+                $special_request = new DefinitionRequest($opts, $mh);
+                break;
+            case 3:
+                require "engines/special/ip.php";
+                $special_request = new IPRequest($opts, $mh);
+                break;
+            case 4:
+                require "engines/special/user_agent.php";
+                $special_request = new UserAgentRequest($opts, $mh);
+                break;
+            case 5:
+                require "engines/special/weather.php";
+                $special_request = new WeatherRequest($opts, $mh);
+                break;
+            case 6:
+                require "engines/special/tor.php";
+                $special_request = new TorRequest($opts, $mh);
+                break;
+            case 7:
+                require "engines/special/wikipedia.php";
+                $special_request = new WikipediaRequest($opts, $mh);
+                break;
+        }
+
+        return $special_request;
+    }
+?>
diff --git a/engines/special/tor.php b/engines/special/tor.php
@@ -1,18 +1,22 @@
 <?php
-    function tor_result($response)
-    {
-            $formatted_response = "It seems like you are not using Tor";
-            if (strpos($response, $_SERVER["REMOTE_ADDR"]) !== false)
-            {
-                $formatted_response = "It seems like you are using Tor";
-            }
 
+    class TorRequest extends EngineRequest {
+        public function get_request_url() {
+            return "https://check.torproject.org/torbulkexitlist";
+        }
+
+        public function get_results() {
+            $response = curl_multi_getcontent($ch);
+
+            $formatted_response = strpos($response, $_SERVER["REMOTE_ADDR"]) ? "It seems like you are using Tor" : "It seems like you are not using Tor";
             $source = "https://check.torproject.org";
+            
             return array(
                 "special_response" => array(
                     "response" => $formatted_response,
                     "source" => $source
                 )
             );
+        }
     }
 ?>
diff --git a/engines/special/user_agent.php b/engines/special/user_agent.php
@@ -1,11 +1,12 @@
 <?php
-    function user_agent_result()
-    {
+    class UserAgentRequest extends EngineRequest {
+        function get_results() {
             return array(
                 "special_response" => array(
                     "response" => $_SERVER["HTTP_USER_AGENT"], 
                     "source" => null
                 )
             );                   
+        }
     }
 ?>
diff --git a/engines/special/weather.php b/engines/special/weather.php
@@ -1,26 +1,31 @@
 <?php
-    function weather_results($response)
-    {
+    class WeatherRequest extends EngineRequest {
+        public function get_request_url () {
+            return "https://wttr.in/@" . $_SERVER["REMOTE_ADDR"] . "?format=j1";
+        }
+        
+        public function get_results() {
+            $response = curl_multi_getcontent($this->ch);
             $json_response = json_decode($response, true);
 
-            if ($json_response)
-            {
-                $current_weather = $json_response["current_condition"][0];
+            if (!$json_response)
+                return array();
 
-                $temp_c = $current_weather["temp_C"];
-                $temp_f = $current_weather["temp_F"];
-                $description = $current_weather["weatherDesc"][0]["value"];
+            $current_weather = $json_response["current_condition"][0];
 
-                $formatted_response = "$description - $temp_c °C | $temp_f °F";
+            $temp_c = $current_weather["temp_C"];
+            $temp_f = $current_weather["temp_F"];
+            $description = $current_weather["weatherDesc"][0]["value"];
 
-                $source = "https://wttr.in";
-                return array(
-                    "special_response" => array(
-                        "response" => htmlspecialchars($formatted_response),
-                        "source" => $source
-                    )
-                );
-            }
+            $formatted_response = "$description - $temp_c °C | $temp_f °F";
 
+            $source = "https://wttr.in";
+            return array(
+                "special_response" => array(
+                    "response" => htmlspecialchars($formatted_response),
+                    "source" => $source
+                )
+            );
+        }
     }
 ?>
diff --git a/engines/special/wikipedia.php b/engines/special/wikipedia.php
@@ -1,21 +1,28 @@
 <?php
-    function wikipedia_results($query, $response) 
-    {
-        global $config;
+    class WikipediaRequest extends EngineRequest {
+        public function get_request_url() {
+                $this->wikipedia_language = $this->opts->language;
+                $query_encoded = urlencode($this->query);
 
-        $query_encoded = urlencode($query);
+                if (!in_array($this->wikipedia_language, json_decode(file_get_contents("static/misc/wikipedia_langs.json"), true)))
+                    $this->wikipedia_language = "en";
 
-        $json_response = json_decode($response, true);
+                return "https://$this->wikipedia_language.wikipedia.org/w/api.php?format=json&action=query&prop=extracts%7Cpageimages&exintro&explaintext&redirects=1&pithumbsize=500&titles=$query_encoded";
+        }
 
-        $first_page = array_values($json_response["query"]["pages"])[0];
+        public function get_results() {
+            $response = curl_multi_getcontent($this->ch);
 
-        if (!array_key_exists("missing", $first_page))
-        {
-            $description = substr($first_page["extract"], 0, 250) . "...";
+            $json_response = json_decode($response, true);
+
+            $first_page = array_values($json_response["query"]["pages"])[0];
 
-            $wikipedia_language = isset($_COOKIE["wikipedia_language"]) ? trim(htmlspecialchars($_COOKIE["wikipedia_language"])) : $config->wikipedia_language;
+            if (array_key_exists("missing", $first_page))
+                return array();
+
+            $description = substr($first_page["extract"], 0, 250) . "...";
 
-            $source = check_for_privacy_frontend("https://$wikipedia_language.wikipedia.org/wiki/$query");
+            $source = check_for_privacy_frontend("https://$this->wikipedia_language.wikipedia.org/wiki/$this->query", $this->opts);
             $response = array(
                 "special_response" => array(
                     "response" => htmlspecialchars($description),
@@ -23,8 +30,7 @@
                 )
             );
 
-            if (array_key_exists("thumbnail",  $first_page))
-            {
+            if (array_key_exists("thumbnail",  $first_page)) {
                 $image_url = $first_page["thumbnail"]["source"];
                 $response["special_response"]["image"] = $image_url;
             }
diff --git a/engines/text/duckduckgo.php b/engines/text/duckduckgo.php
@@ -0,0 +1,64 @@
+<?php
+    class DuckDuckGoRequest extends EngineRequest {
+        function get_request_url()
+        {
+            $query_encoded = str_replace("%22", "\"", urlencode($this->query));
+            $results = array();
+
+            $domain = 'com';
+            $results_language = $this->opts->language;
+            $number_of_results = $this->opts->number_of_results;
+
+            $url = "https://html.duckduckgo.$domain/html/?q=$query_encoded&kd=-1&s=" . 3 * $this->page;
+
+            if (3 > strlen($results_language) && 0 < strlen($results_language))
+                $url .= "&lr=lang_$results_language";
+
+            if (3 > strlen($number_of_results) && 0 < strlen($number_of_results))
+                $url .= "&num=$number_of_results";
+
+            if (isset($_COOKIE["safe_search"]))
+                $url .= "&safe=medium";
+            return $url;
+        }
+
+        public function get_results() {
+            $results = array();
+            $xpath = get_xpath(curl_multi_getcontent($this->ch));
+            
+            foreach($xpath->query("/html/body/div[1]/div[". count($xpath->query('/html/body/div[1]/div')) ."]/div/div/div/div") as $result)
+            {
+                $url = $xpath->evaluate(".//h2[@class='result__title']//a/@href", $result)[0];
+                
+                if ($url == null)
+                    continue;
+
+                if (!empty($results)) // filter duplicate results
+                {
+                    if (end($results)["url"] == $url->textContent)
+                        continue;
+                }
+
+                $url = $url->textContent;
+
+                $url = check_for_privacy_frontend($url, $this->opts);
+
+                $title = $xpath->evaluate(".//h2[@class='result__title']", $result)[0];
+                $description = $xpath->evaluate(".//a[@class='result__snippet']", $result)[0];
+
+                array_push($results,
+                    array (
+                        "title" => htmlspecialchars($title->textContent),
+                        "url" =>  htmlspecialchars($url),
+                        "base_url" => htmlspecialchars(get_base_url($url)),
+                        "description" =>  $description == null ?
+                                          "No description was provided for this site." :
+                                          htmlspecialchars($description->textContent)
+                    )
+                );
+           }
+            return $results;
+        }
+
+    }
+?>
diff --git a/engines/text/google.php b/engines/text/google.php
@@ -0,0 +1,77 @@
+<?php
+    class GoogleRequest extends EngineRequest {
+        function get_request_url()
+        {
+
+            $query_encoded = str_replace("%22", "\"", urlencode($this->query));
+            $results = array();
+
+            $domain = $this->opts->google_domain;
+            $results_language = $this->opts->language;
+            $number_of_results = $this->opts->number_of_results;
+
+            $url = "https://www.google.$domain/search?q=$query_encoded&nfpr=1&start=$this->page";
+
+            if (3 > strlen($results_language) && 0 < strlen($results_language)) {
+                $url .= "&lr=lang_$results_language";
+                $url .= "&hl=$results_language";
+            }
+
+            if (3 > strlen($number_of_results) && 0 < strlen($number_of_results))
+                $url .= "&num=$number_of_results";
+
+            if (isset($_COOKIE["safe_search"]))
+                $url .= "&safe=medium";
+
+            return $url;
+        }
+
+
+        public function get_results() {
+            $results = array();
+            $xpath = get_xpath(curl_multi_getcontent($this->ch));
+
+            if (!$xpath)
+                return $results;
+
+            $didyoumean = $xpath->query(".//a[@class='gL9Hy']")[0];
+
+            if (!is_null($didyoumean))
+                array_push($results, array(
+                    "did_you_mean" => $didyoumean->textContent
+                ));
+
+            foreach($xpath->query("//div[@id='search']//div[contains(@class, 'g')]") as $result) {
+                $url = $xpath->evaluate(".//div[@class='yuRUbf']//a/@href", $result)[0];
+
+                if ($url == null)
+                    continue;
+
+                if (!empty($results)) // filter duplicate results, ignore special result
+                {
+                    if (end($results)["url"] == $url->textContent)
+                        continue;
+                }
+
+                $url = $url->textContent;
+                $url = check_for_privacy_frontend($url, $this->opts);
+
+                $title = $xpath->evaluate(".//h3", $result)[0];
+                $description = $xpath->evaluate(".//div[contains(@class, 'VwiC3b')]", $result)[0];
+
+                array_push($results,
+                    array (
+                        "title" => htmlspecialchars($title->textContent),
+                        "url" =>  htmlspecialchars($url),
+                        "base_url" => htmlspecialchars(get_base_url($url)),
+                        "description" =>  $description == null ?
+                                          "No description was provided for this site." :
+                                          htmlspecialchars($description->textContent)
+                    )
+                );
+            }
+
+            return $results;
+        }
+    }
+?>
diff --git a/engines/text/text.php b/engines/text/text.php
@@ -0,0 +1,135 @@
+<?php
+    class TextSearch extends EngineRequest {
+        public function __construct($opts, $mh) {
+            $this->query = $opts->query;
+            $this->page = $opts->page;
+            $this->opts = $opts;
+
+            $engine = $opts->preferred_engines["text"] ?? "google";
+
+            $query_parts = explode(" ", $this->query);
+            $last_word_query = end($query_parts);
+            if (substr($this->query, 0, 1) == "!" || substr($last_word_query, 0, 1) == "!")
+                check_ddg_bang($this->query, $opts);
+
+            if ($engine == "google") {
+                require "engines/text/google.php";
+                $this->engine_request = new GoogleRequest($opts, $mh);
+            }
+
+            if ($engine == "duckduckgo") {
+                require "engines/text/duckduckgo.php";
+                $this->engine_request = new DuckDuckGoRequest($opts, $mh);
+            }
+
+            require "engines/special/special.php";
+            $this->special_request = get_special_search_request($opts, $mh);
+        }
+
+        public function get_results() {
+            $results = $this->engine_request->get_results();
+
+            if ($this->special_request) {
+                $special_result = $this->special_request->get_results();
+
+                if ($special_result)
+                    $results = array_merge(array($special_result), $results);
+            }
+
+            return $results;
+        }
+
+        public static function print_results($results) {
+
+            if (empty($results))
+                return;
+
+            $special = $results[0];
+
+            if (array_key_exists("did_you_mean", $special)) 
+            {
+                $didyoumean = $special["did_you_mean"];
+                $new_url = "/search.php?q="  . urlencode($didyoumean);
+                echo "<p class=\"did-you-mean\">Did you mean ";
+                echo "<a href=\"$new_url\">$didyoumean</a>";
+                echo "?</p>";
+            }
+
+            if (array_key_exists("special_response", $special)) 
+            {
+                $response = $special["special_response"]["response"];
+                $source = $special["special_response"]["source"];
+
+                echo "<p class=\"special-result-container\">";
+                if (array_key_exists("image", $special["special_response"]))
+                {
+                    $image_url = $special["special_response"]["image"];
+                    echo "<img src=\"image_proxy.php?url=$image_url\">";
+                }
+                echo $response;
+                if ($source)
+                    echo "<a href=\"$source\" target=\"_blank\">$source</a>";
+                echo "</p>";
+            }
+
+            echo "<div class=\"text-result-container\">";
+
+            foreach($results as $result)
+            {
+                if (!array_key_exists("title", $result))
+                    continue;
+
+                $title = $result["title"];
+                $url = $result["url"];
+                $base_url = $result["base_url"];
+                $description = $result["description"];
+
+                echo "<div class=\"text-result-wrapper\">";
+                echo "<a href=\"$url\">";
+                echo "$base_url";
+                echo "<h2>$title</h2>";
+                echo "</a>";
+                echo "<span>$description</span>";
+                echo "</div>";
+            }
+
+            echo "</div>";
+        }
+    }
+
+    function check_ddg_bang($query, $opts)
+    {
+
+        $bangs_json = file_get_contents("static/misc/ddg_bang.json");
+        $bangs = json_decode($bangs_json, true);
+
+        if (substr($query, 0, 1) == "!")
+            $search_word = substr(explode(" ", $query)[0], 1);
+        else
+            $search_word = substr(end(explode(" ", $query)), 1);
+        
+        $bang_url = null;
+
+        foreach($bangs as $bang)
+        {
+            if ($bang["t"] == $search_word)
+            {
+                $bang_url = $bang["u"];
+                break;
+            }
+        }
+
+        if ($bang_url)
+        {
+            $bang_query_array = explode("!" . $search_word, $query);
+            $bang_query = trim(implode("", $bang_query_array));
+
+            $request_url = str_replace("{{{s}}}", str_replace('%26quot%3B','%22', urlencode($bang_query)), $bang_url);
+            $request_url = check_for_privacy_frontend($request_url, $opts);
+
+            header("Location: " . $request_url);
+            die();
+        }
+    }
+
+?>
diff --git a/instances.php b/instances.php
@@ -19,7 +19,7 @@
 
         foreach($instances as $instance) {
             $hostname = parse_url($instance["clearnet"])["host"];
-            $country = get_country_emote($instance["country"]) . $instnace["country"];
+            $country = get_country_emote($instance["country"]) . $instance["country"];
 
             $is_tor = !is_null($instance["tor"]);
             $is_i2p = !is_null($instance["i2p"]);
diff --git a/misc/header.php b/misc/header.php
@@ -8,9 +8,6 @@
         <link rel="stylesheet" type="text/css" href="static/css/styles.css"/>
         <link title="LibreY search" type="application/opensearchdescription+xml" href="opensearch.xml?method=POST" rel="search"/>
         <link rel="stylesheet" type="text/css" href="<?php
-                echo "static/css/";
-                if (isset($_COOKIE["theme"]))
-                    echo htmlspecialchars($_COOKIE["theme"] . ".css");
-                else
-                    echo "dark.css";
+$theme = $_REQUEST["theme"] ?? trim(htmlspecialchars($_COOKIE["theme"] ?? "dark"));
+                echo "static/css/" . $theme . ".css";
         ?>"/>
diff --git a/misc/search_engine.php b/misc/search_engine.php
@@ -0,0 +1,138 @@
+<?php
+    abstract class EngineRequest {
+        function __construct($opts, $mh) {
+            $this->query = $opts->query;
+            $this->page = $opts->page;
+            $this->opts = $opts;
+
+            $url = $this->get_request_url();
+            if (!$url)
+                return;
+
+            $this->ch = curl_init($url);
+
+            if ($opts->curl_settings)
+                curl_setopt_array($this->ch, $opts->curl_settings);
+
+            if ($mh)
+                curl_multi_add_handle($mh, $this->ch);
+        }
+
+        public function get_request_url() {
+            return "";
+        }
+
+        public function successful() {
+            return curl_getinfo($this->ch)['http_code'] == '200';
+        }
+
+        abstract function get_results();
+        static public function print_results($results){}
+    }
+
+    function load_opts() {
+        $opts = require "config.php";
+
+        $opts->query = trim($_REQUEST["q"] ?? "");
+        $opts->type = (int) ($_REQUEST["t"] ?? 0);
+        $opts->page = (int) ($_REQUEST["p"] ?? 0);
+
+        $opts->theme = $_REQUEST["theme"] ?? trim(htmlspecialchars($_COOKIE["theme"] ?? "dark"));
+
+        $opts->safe_search = (int) ($_REQUEST["safe"] ?? 0) == 1 || isset($_COOKIE["safe_search"]);
+
+        $opts->disable_special = (int) ($_REQUEST["ns"] ?? 0) == 1 || isset($_COOKIE["disable_special"]);
+
+        $opts->disable_frontends = (int) ($_REQUEST["nf"] ?? 0) == 1 || isset($_COOKIE["disable_frontends"]);
+
+        $opts->language = $_REQUEST["lang"] ?? trim(htmlspecialchars($_COOKIE["language"] ?? ""));
+
+        $opts->do_fallback = (int) ($_REQUEST["nfb"] ?? 0) == 0;
+        if (!$opts->instance_fallback) {
+            $opts->do_fallback = false;
+        }
+
+        $opts->number_of_results ??= trim(htmlspecialchars($_COOKIE["number_of_results"]));
+
+        foreach (array_keys($opts->frontends ?? array()) as $frontend) {
+            $opts->frontends[$frontend]["instance_url"] = $_COOKIE[$frontend] ?? $opts->frontends[$frontend]["instance_url"];
+        }
+        return $opts;
+    }
+
+    function opts_to_params($opts) {
+        $query = urlencode($opts->query);
+
+        $params = "";
+        $params .= "p=$opts->page";
+        $params .= "&q=$query";
+        $params .= "&t=$opts->type";
+        $params .= "&nfb=" . ($opts->do_fallback ? 0 : 1);
+        $params .= "&safe=" . ($opts->safe_search ? 1 : 0);
+        $params .= "&nf=" . ($opts->disable_frontends ? 1 : 0);
+        $params .= "&ns=" . ($opts->disable_special ? 1 : 0);
+
+        return $params;
+    }
+
+    function init_search($opts, $mh) {
+        switch ($opts->type)
+        {
+            case 1:
+                require "engines/qwant/image.php";
+                return new QwantImageSearch($opts, $mh);
+
+            case 2:
+                require "engines/invidious/video.php";
+                return new VideoSearch($opts, $mh);
+
+            case 3:
+                if ($opts->disable_bittorent_search) {
+                    echo "<p class=\"text-result-container\">The host disabled this feature! :C</p>";
+                    break;
+                }
+
+                require "engines/bittorrent/merge.php";
+                return new TorrentSearch($opts, $mh);
+
+            case 4:
+                if ($opts->disable_hidden_service_search) {
+                    echo "<p class=\"text-result-container\">The host disabled this feature! :C</p>";
+                    break;
+                }
+                require "engines/ahmia/hidden_service.php";
+                return new TorSearch($opts, $mh);
+
+            default:
+                require "engines/text/text.php";
+                return new TextSearch($opts, $mh);
+        }
+    }
+
+    function fetch_search_results($opts, $do_print) {
+        $start_time = microtime(true);
+        $mh = curl_multi_init();
+        $search_category = init_search($opts, $mh);
+
+        $running = null;
+
+        do {
+            curl_multi_exec($mh, $running);
+        } while ($running);
+
+        $results = $search_category->get_results();
+
+        if (count($results) <= 1) {
+            require "engines/librex/fallback.php";
+            $results = get_librex_results($opts);
+        }
+
+        if (!$do_print)
+            return $results;
+
+        print_elapsed_time($start_time);
+        $search_category->print_results($results);
+
+        return $results;
+    }
+?>
diff --git a/misc/tools.php b/misc/tools.php
@@ -1,69 +1,49 @@
 <?php
-    function get_base_url($url)
-    {
+    function get_base_url($url) {
         $split_url = explode("/", $url);
         $base_url = $split_url[0] . "//" . $split_url[2] . "/";
         return $base_url;
     }
 
-    function get_root_domain($url)
-    {
+    function get_root_domain($url) {
         $split_url = explode("/", $url);
         $base_url = $split_url[2];
 
         $base_url_main_split = explode(".", strrev($base_url));
         $root_domain = strrev($base_url_main_split[1]) . "." . strrev($base_url_main_split[0]);
-    
+
         return $root_domain;
     }
 
-    function try_replace_with_frontend($url, $frontend, $original)
-    {
-        global $config;
-        $frontends = $config->frontends;
+    function try_replace_with_frontend($url, $frontend, $original, $opts) {
+        $frontends = $opts->frontends;
 
-        if (isset($_COOKIE[$frontend]) || !empty($frontends[$frontend]["instance_url"]))
-        {
-            
-            if (isset($_COOKIE[$frontend]))
-                $frontend = $_COOKIE[$frontend];
-            else if (!empty($frontends[$frontend]["instance_url"]))
-                $frontend = $frontends[$frontend]["instance_url"];
+        if (array_key_exists($frontend, $opts->frontends)) {
+            $frontend = $frontends[$frontend]["instance_url"];
 
             if (empty(trim($frontend)))
                 return $url;
 
-            if (strpos($url, "wikipedia.org") !== false)
-            {
+            if (strpos($url, "wikipedia.org") !== false) {
                 $wiki_split = explode(".", $url);
-                if (count($wiki_split) > 1)
-                {
+                if (count($wiki_split) > 1) {
                     $lang = explode("://", $wiki_split[0])[1];
                     $url =  $frontend . explode($original, $url)[1] . (strpos($url, "?") !== false ? "&" : "?")  . "lang=" . $lang;
                 }
-            }
-            else if (strpos($url, "fandom.com") !== false)
-            {
+            } else if (strpos($url, "fandom.com") !== false) {
                 $fandom_split = explode(".", $url);
-                if (count($fandom_split) > 1)
-                {
+                if (count($fandom_split) > 1) {
                     $wiki_name = explode("://", $fandom_split[0])[1];
                     $url =  $frontend . "/" . $wiki_name . explode($original, $url)[1];
                 }
-            }
-            else if (strpos($url, "gist.github.com") !== false)
-            {
+            } else if (strpos($url, "gist.github.com") !== false) {
                 $gist_path = explode("gist.github.com", $url)[1];
                 $url = $frontend . "/gist" . $gist_path;
-            }
-            else if (strpos($url, "stackexchange.com") !== false)
-            {
+            } else if (strpos($url, "stackexchange.com") !== false) {
                 $se_domain = explode(".", explode("://", $url)[1])[0];
                 $se_path = explode("stackexchange.com", $url)[1];
                 $url = $frontend . "/exchange" . "/" . $se_domain . $se_path;
-            }
-            else
-            {
+            } else {
                 $url =  $frontend . explode($original, $url)[1];
             }
 
@@ -74,26 +54,18 @@
         return $url;
     }
 
-    function check_for_privacy_frontend($url)
-    {
-
-        global $config;
-
-        if (isset($_COOKIE["disable_frontends"]))
+    function check_for_privacy_frontend($url, $opts) {
+        if ($opts->disable_frontends)
             return $url;
 
-        foreach($config->frontends as $frontend => $data)
-        {
+        foreach($opts->frontends as $frontend => $data) {
             $original = $data["original_url"];
 
-            if (strpos($url, $original))
-            {
-                $url = try_replace_with_frontend($url, $frontend, $original);
+            if (strpos($url, $original)) {
+                $url = try_replace_with_frontend($url, $frontend, $original, $opts);
                 break;
-            }
-            else if (strpos($url, "stackexchange.com"))
-            {
-                $url = try_replace_with_frontend($url, "anonymousoverflow", "stackexchange.com");
+            } else if (strpos($url, "stackexchange.com")) {
+                $url = try_replace_with_frontend($url, "anonymousoverflow", "stackexchange.com", $opts);
                 break;
             }
         }
@@ -101,88 +73,10 @@
         return $url;
     }
 
-    function check_ddg_bang($query)
-    {
-
-        $bangs_json = file_get_contents("static/misc/ddg_bang.json");
-        $bangs = json_decode($bangs_json, true);
-
-        if (substr($query, 0, 1) == "!")
-            $search_word = substr(explode(" ", $query)[0], 1);
-        else
-            $search_word = substr(end(explode(" ", $query)), 1);
-        
-        $bang_url = null;
-
-        foreach($bangs as $bang)
-        {
-            if ($bang["t"] == $search_word)
-            {
-                $bang_url = $bang["u"];
-                break;
-            }
-        }
-
-        if ($bang_url)
-        {
-            $bang_query_array = explode("!" . $search_word, $query);
-            $bang_query = trim(implode("", $bang_query_array));
-
-            $request_url = str_replace("{{{s}}}", str_replace('%26quot%3B','%22', urlencode($bang_query)), $bang_url);
-            $request_url = check_for_privacy_frontend($request_url);
-
-            header("Location: " . $request_url);
-            die();
-        }
-    }
-
-    function check_for_special_search($query)
-    {
-        if (isset($_COOKIE["disable_special"]))
-            return 0;
-
-         $query_lower = strtolower($query);
-         $split_query = explode(" ", $query);
-
-         if (strpos($query_lower, "to") && count($split_query) >= 4) // currency
-         {
-            $amount_to_convert = floatval($split_query[0]);
-            if ($amount_to_convert != 0)
-                return 1;
-         }
-         else if (strpos($query_lower, "mean") && count($split_query) >= 2) // definition
-         {
-             return 2;
-         }
-         else if (strpos($query_lower, "my") !== false)
-         {
-            if (strpos($query_lower, "ip"))
-            {
-                return 3;
-            }
-            else if (strpos($query_lower, "user agent") || strpos($query_lower, "ua"))
-            {
-                return 4;
-            }
-         }
-         else if (strpos($query_lower, "weather") !== false)
-         {
-                return 5;
-         }
-         else if ($query_lower == "tor")
-         {
-                return 6;
-         }
-         else if (3 > count(explode(" ", $query))) // wikipedia
-         {
-             return 7;
-         }
-
-        return 0;
-    }
+    function get_xpath($response) {
+        if (!$response)
+            return null;
 
-    function get_xpath($response)
-    {
         $htmlDom = new DOMDocument;
         @$htmlDom->loadHTML($response);
         $xpath = new DOMXPath($htmlDom);
@@ -190,9 +84,8 @@
         return $xpath;
     }
 
-    function request($url)
-    {
-        global $config;
+    function request($url) {
+        $config ??= require "config.php";
 
         $ch = curl_init($url);
         curl_setopt_array($ch, $config->curl_settings);
@@ -201,28 +94,24 @@
         return $response;
     }
 
-    function human_filesize($bytes, $dec = 2)
-    {
+    function human_filesize($bytes, $dec = 2) {
         $size   = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
         $factor = floor((strlen($bytes) - 1) / 3);
 
         return sprintf("%.{$dec}f ", $bytes / pow(1024, $factor)) . @$size[$factor];
     }
 
-    function remove_special($string)
-    {
+    function remove_special($string) {
         $string = preg_replace("/[\r\n]+/", "\n", $string);
         return trim(preg_replace("/\s+/", ' ', $string));
      }
 
-    function print_elapsed_time($start_time)
-        {
+    function print_elapsed_time($start_time) {
             $end_time = number_format(microtime(true) - $start_time, 2, '.', '');
             echo "<p id=\"time\">Fetched the results in $end_time seconds</p>";
         }
 
-    function print_next_page_button($text, $page, $query, $type)
-    {
+    function print_next_page_button($text, $page, $query, $type) {
         echo "<form class=\"page\" action=\"search.php\" target=\"_top\" method=\"get\" autocomplete=\"off\">";
         echo "<input type=\"hidden\" name=\"p\" value=\"" . $page . "\" />";
         echo "<input type=\"hidden\" name=\"q\" value=\"$query\" />";
@@ -231,15 +120,13 @@
         echo "</form>";
     }
 
-    function copy_cookies($curl)
-    {
+    function copy_cookies($curl) {
         if (array_key_exists("HTTP_COOKIE", $_SERVER))
             curl_setopt( $curl, CURLOPT_COOKIE, $_SERVER['HTTP_COOKIE'] );
     }
 
     
-    function get_country_emote($code)
-    {
+    function get_country_emote($code) {
         $emoji = [];
         foreach(str_split($code) as $c) {
             if(($o = ord($c)) > 64 && $o % 32 < 27) {
diff --git a/search.php b/search.php
@@ -1,14 +1,34 @@
-<?php 
+<?php
     require "misc/header.php";
 
-    $config = require "config.php";
     require "misc/tools.php";
+    require "misc/search_engine.php";
+
+    $opts = load_opts();
+
+    function print_page_buttons($type, $query, $page) {
+        if ($type > 1)
+            return;
+        echo "<div class=\"next-page-button-wrapper\">";
+
+            if ($page != 0)
+            {
+                print_next_page_button("&lt;&lt;", 0, $query, $type);
+                print_next_page_button("&lt;", $page - 10, $query, $type);
+            }
+
+            for ($i=$page / 10; $page / 10 + 10 > $i; $i++)
+                print_next_page_button($i + 1, $i * 10, $query, $type);
+
+            print_next_page_button("&gt;", $page + 10, $query, $type);
+
+        echo "</div>";
+    }
 ?>
 
 <title>
 <?php
-  $query = trim($_REQUEST["q"]);
-  echo $query;
+    echo $opts->query;
 ?> - LibreY</title>
 </head>
     <body>
@@ -16,21 +36,18 @@
             <h1 class="logomobile"><a class="no-decoration" href="./">Libre<span class="Y">Y</span></a></h1>
             <input type="text" name="q"
                 <?php
-                    $query_encoded = urlencode($query);
-
-                    if (1 > strlen($query) || strlen($query) > 256)
+                    if (1 > strlen($opts->query) || strlen($opts->query) > 256)
                     {
                         header("Location: ./");
                         die();
                     }
 
-                    echo "value=\"" . htmlspecialchars($query) . "\"";
+                    echo "value=\"" . htmlspecialchars($opts->query) . "\"";
                 ?>
             >
             <br>
             <?php
-                $type = isset($_REQUEST["t"]) ? (int) $_REQUEST["t"] : 0;
-                echo "<button class=\"hide\" name=\"t\" value=\"$type\"/></button>";
+                echo "<button class=\"hide\" name=\"t\" value=\"$opts->type\"/></button>";
             ?>
             <button type="submit" class="hide"></button>
             <input type="hidden" name="p" value="0">
@@ -42,13 +59,13 @@
                     {
                         $category_index = array_search($category, $categories);
 
-                        if (($config->disable_bittorent_search && $category_index == 3) ||
-                            ($config->disable_hidden_service_search && $category_index ==4))
+                        if (($opts->disable_bittorent_search && $category_index == 3) ||
+                            ($opts->disable_hidden_service_search && $category_index ==4))
                         {
                             continue;
                         }
 
-                        echo "<a " . (($category_index == $type) ? "class=\"active\" " : "") . "href=\"./search.php?q=" . $query . "&p=0&t=" . $category_index . "\"><img src=\"static/images/" . $category . "_result.png\" alt=\"" . $category . " result\" />" . ucfirst($category)  . "</a>";
+                        echo "<a " . (($category_index == $opts->type) ? "class=\"active\" " : "") . "href=\"./search.php?q=" . urlencode($opts->query) . "&p=0&t=" . $category_index . "\"><img src=\"static/images/" . $category . "_result.png\" alt=\"" . $category . " result\" />" . ucfirst($category)  . "</a>";
                     }
                 ?>
             </div>
@@ -56,93 +73,8 @@
         </form>
 
         <?php
-
-            $page = isset($_REQUEST["p"]) ? (int) $_REQUEST["p"] : 0;
-            $start_time = microtime(true);
-            switch ($type)
-            {
-                case 0:
-					$engine=$config->preferred_engines['text'];
-                    if (is_null($engine))
-                        $engine = "google";
-                    $query_parts = explode(" ", $query);
-                    $last_word_query = end($query_parts);
-                    if (substr($query, 0, 1) == "!" || substr($last_word_query, 0, 1) == "!")
-                        check_ddg_bang($query);
-					require "engines/$engine/text.php";
-                    $results = get_text_results($query, $page);
-                    print_elapsed_time($start_time);
-                    print_text_results($results);
-                    break;
-
-                case 1:
-                    require "engines/qwant/image.php";
-                    $results = get_image_results($query_encoded, $page);
-                    print_elapsed_time($start_time);
-                    print_image_results($results);
-                    break;
-
-                case 2:
-                    require "engines/invidious/video.php";
-                    $results = get_video_results($query_encoded);
-                    print_elapsed_time($start_time);
-                    print_video_results($results);
-                    break;
-
-                case 3:
-                    if ($config->disable_bittorent_search)
-                        echo "<p class=\"text-result-container\">The host disabled this feature! :C</p>";
-                    else
-                    {
-                        require "engines/bittorrent/merge.php";
-                        $results = get_merged_torrent_results($query_encoded);
-                        print_elapsed_time($start_time);
-                        print_merged_torrent_results($results);
-                    }
-                    break;
-
-                case 4:
-                    if ($config->disable_hidden_service_search)
-                        echo "<p class=\"text-result-container\">The host disabled this feature! :C</p>";
-                    else
-                    {
-                        require "engines/ahmia/hidden_service.php";
-                        $results = get_hidden_service_results($query_encoded);
-                        print_elapsed_time($start_time);
-                        print_hidden_service_results($results);
-                    }
-                    break;
-
-                default:
-                    $query_parts = explode(" ", $query);
-                    $last_word_query = end($query_parts);
-                    if (substr($query, 0, 1) == "!" || substr($last_word_query, 0, 1) == "!")
-                        check_ddg_bang($query);
-                    require "engines/google/text.php";
-                    $results = get_text_results($query, $page);
-                    print_elapsed_time($start_time);
-                    print_text_results($results);
-                    break;
-            }
-
-
-            if (2 > $type)
-            {
-                echo "<div class=\"next-page-button-wrapper\">";
-
-                    if ($page != 0)
-                    {
-                        print_next_page_button("&lt;&lt;", 0, $query, $type);
-                        print_next_page_button("&lt;", $page - 10, $query, $type);
-                    }
-
-                    for ($i=$page / 10; $page / 10 + 10 > $i; $i++)
-                        print_next_page_button($i + 1, $i * 10, $query, $type);
-
-                    print_next_page_button("&gt;", $page + 10, $query, $type);
-
-                echo "</div>";
-            }
+            fetch_search_results($opts, true);
+            print_page_buttons($opts->type, $opts->query, $opts->page);
         ?>
 
 <?php require "misc/footer.php"; ?>
diff --git a/settings.php b/settings.php
@@ -1,43 +1,35 @@
 <?php
-                $config = require "config.php";
+        require "misc/search_engine.php";
+        $opts = load_opts();
 
-                // Reset all cookies when resetting, or before saving new cookies
-                if (isset($_REQUEST["reset"]) || isset($_REQUEST["save"]))
-                {
-                    if (isset($_SERVER["HTTP_COOKIE"]))
-                    {
-                        $cookies = explode(";", $_SERVER["HTTP_COOKIE"]);
-                        foreach($cookies as $cookie)
-                        {
-                            $parts = explode("=", $cookie);
-                            $name = trim($parts[0]);
-                            setcookie($name, "", time() - 1000);
-                        }
-                    }
+        // Reset all cookies when resetting, or before saving new cookies
+        if (isset($_REQUEST["reset"]) || isset($_REQUEST["save"])) {
+            if (isset($_SERVER["HTTP_COOKIE"])) {
+                $cookies = explode(";", $_SERVER["HTTP_COOKIE"]);
+                foreach($cookies as $cookie) {
+                    $parts = explode("=", $cookie);
+                    $name = trim($parts[0]);
+                    setcookie($name, "", time() - 1000);
                 }
+            }
+        }
 
-                if (isset($_REQUEST["save"]))
-                {
-                    foreach($_POST as $key=>$value)
-                    {
-                        if (!empty($value))
-                        {
-                            setcookie($key, $value, time() + (86400 * 90), '/');
-                        }
-                        else
-                        {
-                            setcookie($key, "", time() - 1000);
-                        }
-                    }
+        if (isset($_REQUEST["save"])) {
+            foreach($_POST as $key=>$value) {
+                if (!empty($value)) {
+                    setcookie($key, $value, time() + (86400 * 90), '/');
+                } else {
+                    setcookie($key, "", time() - 1000);
                 }
+            }
+        }
 
-                if (isset($_REQUEST["save"]) || isset($_REQUEST["reset"]))
-                {
-                    header("Location: ./");
-                    die();
-                }
+        if (isset($_REQUEST["save"]) || isset($_REQUEST["reset"])) {
+            header("Location: ./");
+            die();
+        }
 
-                require "misc/header.php";
+        require "misc/header.php";
 ?>
 
     <title>LibreY - Settings</title>
@@ -67,10 +59,9 @@
                     <option value=\"ubuntu\">Ubuntu</option>
                     <option value=\"tokyo_night\">Tokyo night</option>";
 
-                    if (isset($_COOKIE["theme"]))
-                    {
-                        $cookie_theme = $_COOKIE["theme"];
-                        $themes = str_replace($cookie_theme . "\"", $cookie_theme . "\" selected", $themes);
+                    if (isset($_COOKIE["theme"])) {
+                        $theme = $opts->theme;
+                        $themes = str_replace($theme . "\"", $theme . "\" selected", $themes);
                     }
 
                     echo $themes;
@@ -79,19 +70,19 @@
                 </div>
                 <div>
                     <label>Disable special queries (e.g.: currency conversion)</label>
-                    <input type="checkbox" name="disable_special" <?php echo isset($_COOKIE["disable_special"]) ? "checked"  : ""; ?> >
+                    <input type="checkbox" name="disable_special" <?php echo $opts->disable_special ? "checked"  : ""; ?> >
                 </div>
 
                 <h2>Privacy friendly frontends</h2>
                 <p>For an example if you want to view YouTube without getting spied on, click on "Invidious", find the instance that is most suitable for you then paste it in (correct format: https://example.com)</p>
                 <div class="settings-textbox-container">
                       <?php
-                           foreach($config->frontends as $frontend => $data)
+                           foreach($opts->frontends as $frontend => $data)
                            {
                                 echo "<div>";
                                 echo "<a for=\"$frontend\" href=\"" . $data["project_url"] . "\" target=\"_blank\">" . ucfirst($frontend) . "</a>";
                                 echo "<input type=\"text\" name=\"$frontend\" placeholder=\"Replace " .  $data["original_name"] . "\" value=";
-                                echo isset($_COOKIE["$frontend"]) ? htmlspecialchars($_COOKIE["$frontend"]) :  $data["instance_url"];
+                                echo htmlspecialchars($opts->frontends["$frontend"]["instance_url"] ?? "");
                                 echo ">";
                                 echo "</div>";
                            }
@@ -99,43 +90,25 @@
                 </div>
                 <div>
                     <label>Disable frontends</label>
-                    <input type="checkbox" name="disable_frontends" <?php echo isset($_COOKIE["disable_frontends"]) ? "checked"  : ""; ?> >
+                    <input type="checkbox" name="disable_frontends" <?php echo $opts->disable_frontends ? "checked"  : ""; ?> >
                 </div>
 
                 <h2>Search settings</h2>
                 <div class="settings-textbox-container">
                     <div>
-                        <span>Site language</span>
-                        <?php
-                            echo "<input type=\"text\" name=\"google_language_site\" placeholder=\"E.g.: en\" value=\"";
-                            echo isset($_COOKIE["google_language_site"]) ? htmlspecialchars($_COOKIE["google_language_site"]) : $config->google_language_site;
-                        ?>">
-                    </div>
-                    <div>
-                        <span>Results language</span>
+                        <span>Language</span>
                         <?php
-                            echo "<input type=\"text\" name=\"google_language_results\" placeholder=\"E.g.: de\" value=\"";
-                            echo isset($_COOKIE["google_language_results"]) ? htmlspecialchars($_COOKIE["google_language_results"]) : $config->google_language_results;
-                        ?>">
+                            // TODO make this a dropdown
+                            echo "<input type=\"text\" name=\"language\" placeholder=\"any\" value=\"" . htmlspecialchars($opts->language ?? "") . "\">";
+                        ?>
                     </div>
                     <div>
                         <label>Number of results per page</label>
-                        <input type="number" name="google_number_of_results" value="<?php echo isset($_COOKIE["google_number_of_results"]) ? $_COOKIE["google_number_of_results"]  : $config->google_number_of_results; ?>" >
+                        <input type="number" name="number_of_results" value="<?php echo htmlspecialchars($opts->number_of_results ?? "10") ?>" >
                     </div>
                     <div>
                         <label>Safe search</label>
-                        <input type="checkbox" name="safe_search" <?php echo isset($_COOKIE["safe_search"]) ? "checked"  : ""; ?> >
-                    </div>
-                </div>
-
-                <h2>Wikipedia settings</h2>
-                <div class="settings-textbox-container">
-                    <div>
-                        <span>Results language</span>
-                        <?php
-                            echo "<input type=\"text\" name=\"wikipedia_language\" placeholder=\"E.g.: en\" value=\"";
-                            echo isset($_COOKIE["wikipedia_language"]) ? htmlspecialchars($_COOKIE["wikipedia_language"]) : $config->wikipedia_language;
-                        ?>">
+                        <input type="checkbox" name="safe_search" <?php echo $opts->safe_search ? "checked"  : ""; ?> >
                     </div>
                 </div>