From edc42ea35d05b536c2bebfcb78d1bf6007445e85 Mon Sep 17 00:00:00 2001 From: lolcat Date: Wed, 13 Sep 2023 09:01:23 -0400 Subject: [PATCH] added autocomplete --- api/v1/ac.php | 225 ++++++++++++++++++++++++++++++++++++ scraper/brave.php | 2 - scraper/google.php | 32 +++--- scraper/sc.php | 9 +- settings.php | 91 ++++++++++++--- static/client.js | 266 ++++++++++++++++++++++++++++++++++++++++++- static/style.css | 16 +-- template/header.html | 2 +- template/home.html | 4 +- 9 files changed, 594 insertions(+), 53 deletions(-) create mode 100644 api/v1/ac.php diff --git a/api/v1/ac.php b/api/v1/ac.php new file mode 100644 index 0000000..0964fd9 --- /dev/null +++ b/api/v1/ac.php @@ -0,0 +1,225 @@ +scrapers = [ + "brave" => "https://search.brave.com/api/suggest?q={searchTerms}", + "ddg" => "https://duckduckgo.com/ac/?q={searchTerms}&type=list", + "yandex" => "https://suggest.yandex.com/suggest-ff.cgi?part={searchTerms}&uil=en&v=3&sn=5&lr=21276&yu=4861394161661655015", + "google" => "https://www.google.com/complete/search?client=mobile-gws-lite&q={searchTerms}", + "qwant" => "https://api.qwant.com/v3/suggest/?q={searchTerms}&client=opensearch", + "yep" => "https://api.yep.com/ac/?query={searchTerms}", + "marginalia" => "https://search.marginalia.nu/suggest/?partial={searchTerms}", + "yt" => "https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&q={searchTerms}", + "sc" => "https://api-v2.soundcloud.com/search/queries?q={searchTerms}&client_id=iMxZgT5mfGstBj8GWJbYMvpzelS8ne0E&limit=10&offset=0&linked_partitioning=1&app_version=1693487844&app_locale=en" + ]; + + /* + Sanitize input + */ + if(!isset($_GET["s"])){ + + $this->do404("Missing search(s) parameter"); + } + + if(is_string($_GET["s"]) === false){ + + $this->do404("Invalid search(s) parameter"); + } + + if(strlen($_GET["s"]) > 500){ + + $this->do404("Search(s) exceeds the 500 char length"); + } + + if( + isset($_GET["scraper"]) && + is_string($_GET["scraper"]) === false + ){ + + $_GET["scraper"] = "brave"; // default option + } + + /* + Get $scraper + */ + if(!isset($_GET["scraper"])){ + + if(isset($_COOKIE["scraper_ac"])){ + + $scraper = $_COOKIE["scraper_ac"]; + }else{ + + $scraper = "brave"; // default option + } + }else{ + + $scraper = $_GET["scraper"]; + } + + if($scraper == "disabled"){ + + // this shouldnt happen, but let's handle it anyways + $this->doempty(); + } + + // make sure it exists + if(!isset($this->scrapers[$scraper])){ + + $scraper = "brave"; // default option + } + + // return results + + switch($scraper){ + + case "google": + case "yt": + // handle google cause they want to be a special snowflake :( + $js = $this->get($this->scrapers[$scraper], $_GET["s"]); + + preg_match( + '/\((\[.*\])\)/', + $js, + $js + ); + + if(!isset($js[1])){ + + $this->doempty(); + } + + $js = json_decode($js[1]); + $json = []; + + foreach($js[1] as $item){ + + $json[] = strip_tags($item[0]); + } + + echo json_encode( + [ + $_GET["s"], + $json + ] + ); + break; + + case "sc": + // soundcloud + $js = $this->get($this->scrapers[$scraper], $_GET["s"]); + + $js = json_decode($js, true); + + if(!isset($js["collection"])){ + + $this->doempty(); + } + + $json = []; + foreach($js["collection"] as $item){ + + $json[] = $item["query"]; + } + + echo json_encode( + [ + $_GET["s"], + $json + ] + ); + break; + + case "marginalia": + $json = $this->get($this->scrapers[$scraper], $_GET["s"]); + + $json = json_decode($json, true); + if($json === null){ + + + $this->doempty(); + } + + echo json_encode( + [ + $_GET["s"], + $json + ] + ); + break; + + default: + // if it respects the openSearch protocol + $json = json_decode($this->get($this->scrapers[$scraper], $_GET["s"]), true); + + echo json_encode( + [ + $_GET["s"], + $json[1] // ensure it contains valid key 0 + ] + ); + break; + } + } + + private function get($url, $query){ + + $curlproc = curl_init(); + + $url = str_replace("{searchTerms}", urlencode($query), $url); + + curl_setopt($curlproc, CURLOPT_URL, $url); + + curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding + curl_setopt($curlproc, CURLOPT_HTTPHEADER, + ["User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0", + "Accept: application/json, text/javascript, */*; q=0.01", + "Accept-Language: en-US,en;q=0.5", + "Accept-Encoding: gzip", + "DNT: 1", + "Connection: keep-alive", + "Sec-Fetch-Dest: empty", + "Sec-Fetch-Mode: cors", + "Sec-Fetch-Site: same-site"] + ); + + curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30); + curl_setopt($curlproc, CURLOPT_TIMEOUT, 30); + + $data = curl_exec($curlproc); + + if(curl_errno($curlproc)){ + + throw new Exception(curl_error($curlproc)); + } + + curl_close($curlproc); + return $data; + } + + private function do404($error){ + + echo json_encode(["error" => $error]); + die(); + } + + private function doempty(){ + + echo json_encode( + [ + $_GET["s"], + [] + ] + ); + die(); + } +} diff --git a/scraper/brave.php b/scraper/brave.php index bf11865..93256a8 100644 --- a/scraper/brave.php +++ b/scraper/brave.php @@ -574,8 +574,6 @@ class brave{ } } - echo "test"; - if($rating !== null){ $table["Rating"] = $rating; diff --git a/scraper/google.php b/scraper/google.php index d0e90ca..ca77231 100644 --- a/scraper/google.php +++ b/scraper/google.php @@ -1616,21 +1616,23 @@ class google{ $imgvl ); - $imgvl = $imgvl[1]; - - $params["async"] = "_id:islrg_c,_fmt:html"; - $params["asearch"] = "ichunklite"; - $params["ved"] = $ved; - $params["vet"] = "1" . $ved . "..i"; - $params["start"] = 100; - $params["ijn"] = 1; - $params["imgvl"] = $imgvl; - - $out["npt"] = - $this->nextpage->store( - json_encode($params), - "images" - ); + if(isset($imgvl[1])){ + $imgvl = $imgvl[1]; + + $params["async"] = "_id:islrg_c,_fmt:html"; + $params["asearch"] = "ichunklite"; + $params["ved"] = $ved; + $params["vet"] = "1" . $ved . "..i"; + $params["start"] = 100; + $params["ijn"] = 1; + $params["imgvl"] = $imgvl; + + $out["npt"] = + $this->nextpage->store( + json_encode($params), + "images" + ); + } } } diff --git a/scraper/sc.php b/scraper/sc.php index f297723..1774c20 100644 --- a/scraper/sc.php +++ b/scraper/sc.php @@ -288,7 +288,7 @@ class sc{ if(count($description) != 0){ - $description = $count . " songs. " . implode(", ", $description); + $description = trim($count . " songs. " . implode(", ", $description)); } if( @@ -320,7 +320,7 @@ class sc{ $out["playlist"][] = [ "title" => $item["title"], - "description" => $description, + "description" => $this->limitstrlen($description), "author" => [ "name" => $item["user"]["username"], "url" => $item["user"]["permalink_url"], @@ -385,13 +385,14 @@ class sc{ "\n", wordwrap( str_replace( - "\n", + ["\n\r", "\r\n", "\n", "\r"], " ", $text ), 300, "\n" - ) + ), + 2 )[0]; } } diff --git a/settings.php b/settings.php index f6abb12..bce3af0 100644 --- a/settings.php +++ b/settings.php @@ -58,6 +58,56 @@ $settings = [ [ "name" => "Scrapers to use", "settings" => [ + [ + "description" => "Autocomplete
Picking
Auto
changes the source dynamically depending of the page's scraper
Picking
Disabled
disables this feature
", + "parameter" => "scraper_ac", + "options" => [ + [ + "value" => "disabled", + "text" => "Disabled" + ], + [ + "value" => "auto", + "text" => "Auto" + ], + [ + "value" => "brave", + "text" => "Brave" + ], + [ + "value" => "ddg", + "text" => "DuckDuckGo" + ], + [ + "value" => "yandex", + "text" => "Yandex" + ], + [ + "value" => "google", + "text" => "Google" + ], + [ + "value" => "qwant", + "text" => "Qwant" + ], + [ + "value" => "yep", + "text" => "Yep" + ], + [ + "value" => "marginalia", + "text" => "Marginalia" + ], + [ + "value" => "yt", + "text" => "YouTube" + ], + [ + "value" => "sc", + "text" => "SoundCloud" + ] + ] + ], [ "description" => "Web", "parameter" => "scraper_web", @@ -183,8 +233,13 @@ $settings = [ if($_POST){ $loop = &$_POST; -}else{ +}elseif(count($_GET) !== 0){ + // redirect user to front page + $loop = &$_GET; + header("Location: /"); + +}else{ // refresh cookie dates $loop = &$_COOKIE; } @@ -245,7 +300,7 @@ echo '' . '' . 'Settings' . - '' . + '' . '' . '' . '' . @@ -260,14 +315,14 @@ $left = 'By clicking
Update settings!
, a plaintext
key=value
cookie will be stored on your browser. When selecting a default setting, the parameter is removed from your cookies.'; $c = count($_COOKIE); +$code = ""; + if($c !== 0){ $left .= '

Your current cookie looks like this:' . '
'; - $code = ""; - $ca = 0; foreach($_COOKIE as $key => $value){ @@ -326,17 +381,23 @@ $left .= '
' . '
' . '' . - '< Return to main page' . + '< Return to front page' . '
' . ''; -echo - $frontend->load( - "search.html", - [ - "class" => "", - "right-left" => "", - "right-right" => "", - "left" => $left - ] - ); +if(count($_GET) === 0){ + + echo + $frontend->load( + "search.html", + [ + "class" => "", + "right-left" => + '

Preference link

Follow this link to auto-apply all cookies. Useful if your browser clears out cookies after a browsing session. Following this link will redirect you to the front page, unless no settings are set.

' . + 'Bookmark me!' . + '
', + "right-right" => "", + "left" => $left + ] + ); +} diff --git a/static/client.js b/static/client.js index 89e9a5e..a53cdb6 100644 --- a/static/client.js +++ b/static/client.js @@ -660,15 +660,16 @@ function changeimage(event){ centerpopup(); } -/* - Shortcuts -*/ var searchbox_wrapper = document.getElementsByClassName("searchbox"); if(searchbox_wrapper.length !== 0){ + searchbox_wrapper = searchbox_wrapper[0]; var searchbox = searchbox_wrapper.getElementsByTagName("input")[1]; - + + /* + Textarea shortcuts + */ document.addEventListener("keydown", function(key){ switch(key.keyCode){ @@ -695,4 +696,261 @@ if(searchbox_wrapper.length !== 0){ break; } }); + + /* + Autocompleter + */ + if( // make sure the user wants it + document.cookie.includes("scraper_ac=") && + document.cookie.includes("scraper_ac=disabled") === false + ){ + + var autocomplete_cache = []; + var focuspos = -1; + var list = []; + var autocomplete_div = document.getElementsByClassName("autocomplete")[0]; + + if( + document.cookie.includes("scraper_ac=auto") && + typeof scraper_dropdown != "undefined" + ){ + + var ac_req_appendix = "&scraper=" + scraper_dropdown.value; + }else{ + + var ac_req_appendix = ""; + } + + function getsearchboxtext(){ + + var value = + searchbox.value + .trim() + .replace( + / +/g, + " " + ) + .toLowerCase(); + + return value; + } + + searchbox.addEventListener("input", async function(){ + + // ratelimit on input only + // dont ratelimit if we already have res + if(typeof autocomplete_cache[getsearchboxtext()] != "undefined"){ + + await getac(); + }else{ + + await getac_ratelimit(); + } + }); + + async function getac(){ + + var curvalue = getsearchboxtext(); + + if(curvalue == ""){ + + // hide autocompleter + autocomplete_div.style.display = "none"; + return; + } + + if(typeof autocomplete_cache[curvalue] == "undefined"){ + + /* + Fetch autocomplete + */ + // make sure we dont fetch same thing twice + autocomplete_cache[curvalue] = []; + + var res = await fetch("/api/v1/ac?s=" + encodeURIComponent(curvalue) + ac_req_appendix); + var json = await res.json(); + + autocomplete_cache[curvalue] = json[1]; + + if(curvalue == getsearchboxtext()){ + + render_ac(curvalue, autocomplete_cache[curvalue]); + } + return; + } + + render_ac(curvalue, autocomplete_cache[curvalue]); + } + + var ac_func = null; + function getac_ratelimit(){ + + return new Promise(async function(resolve, reject){ + + if(ac_func !== null){ + + clearTimeout(ac_func); + }//else{ + + // no ratelimits + //getac(); + //} + + ac_func = + setTimeout(function(){ + + ac_func = null; + getac(); // get results after 100ms of no keystroke + resolve(); + }, 300); + }); + } + + function render_ac(query, list){ + + if(list.length === 0){ + + autocomplete_div.style.display = "none"; + return; + } + + html = ""; + + // prepare regex + var highlight = query.split(" "); + var regex = []; + + for(var k=0; k' + + htmlspecialchars( + list[i] + ).replace( + regex, + '$&' + ) + + ''; + } + + autocomplete_div.innerHTML = html; + autocomplete_div.style.display = "block"; + } + + var should_focus = false; + document.addEventListener("keydown", function(event){ + + if(event.key == "Escape"){ + + document.activeElement.blur(); + focuspos = -1; + autocomplete_div.style.display = "none"; + return; + } + + if( + is_click_within(event.target, "searchbox") === false || + typeof autocomplete_cache[getsearchboxtext()] == "undefined" + ){ + + return; + } + + switch(event.key){ + + case "ArrowUp": + event.preventDefault(); + focuspos--; + if(focuspos === -2){ + + focuspos = autocomplete_cache[getsearchboxtext()].length - 1; + } + break; + + case "ArrowDown": + case "Tab": + event.preventDefault(); + + focuspos++; + if(focuspos >= autocomplete_cache[getsearchboxtext()].length){ + + focuspos = -1; + } + break; + + case "Enter": + should_focus = true; + + if(focuspos !== -1){ + + // replace input content + event.preventDefault(); + searchbox.value = + autocomplete_div.getElementsByClassName("entry")[focuspos].innerText; + break; + } + break; + + default: + focuspos = -1; + break; + } + + if(focuspos === -1){ + + searchbox.focus(); + return; + } + + autocomplete_div.getElementsByClassName("entry")[focuspos].focus(); + }); + + window.addEventListener("blur", function(){ + + autocomplete_div.style.display = "none"; + }); + + document.addEventListener("keyup", function(event){ + + // handle ENTER key on entry + if(should_focus){ + + should_focus = false; + searchbox.focus(); + } + }); + + document.addEventListener("mousedown", function(event){ + + // hide input if click is outside + if(is_click_within(event.target, "searchbox") === false){ + + autocomplete_div.style.display = "none"; + return; + } + }); + + function handle_entry_click(event){ + + searchbox.value = event.innerText; + focuspos = -1; + searchbox.focus(); + } + + searchbox.addEventListener("focus", function(){ + + focuspos = -1; + getac(); + }); + } } diff --git a/static/style.css b/static/style.css index ee320a7..ec55624 100644 --- a/static/style.css +++ b/static/style.css @@ -149,31 +149,27 @@ h3,h4,h5,h6{ left:-1px; right:-1px; background:var(--282828); - border:1px solid var(--504945); + border:1px solid var(--928374); border-top:none; border-radius:0 0 2px 2px; z-index:10; + overflow:hidden; } .autocomplete .entry{ overflow:hidden; padding:4px 10px; cursor:pointer; + outline:none; + user-select:none; } .autocomplete .entry:hover{ background:var(--3c3836); } -.autocomplete .title{ - float:left; -} - -.autocomplete .subtext{ - float:right; - font-size:14px; - color:var(--928374); - margin-left:7px; +.autocomplete .entry:focus{ + background:var(--3c3836); } /* Tabs */ diff --git a/template/header.html b/template/header.html index b687d27..9e519fc 100644 --- a/template/header.html +++ b/template/header.html @@ -3,7 +3,7 @@ {%title%} - + diff --git a/template/home.html b/template/home.html index 25267c8..9818677 100644 --- a/template/home.html +++ b/template/home.html @@ -4,7 +4,7 @@ 4get - + @@ -33,6 +33,6 @@ Report a problem: lolcat.ca - +