added autocomplete

This commit is contained in:
lolcat 2023-09-13 09:01:23 -04:00
parent 71a61304b0
commit edc42ea35d
9 changed files with 594 additions and 53 deletions

225
api/v1/ac.php Normal file
View File

@ -0,0 +1,225 @@
<?php
new autocomplete();
class autocomplete{
public function __construct(){
header("Content-Type: application/json");
$this->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();
}
}

View File

@ -574,8 +574,6 @@ class brave{
}
}
echo "test";
if($rating !== null){
$table["Rating"] = $rating;

View File

@ -1616,6 +1616,7 @@ class google{
$imgvl
);
if(isset($imgvl[1])){
$imgvl = $imgvl[1];
$params["async"] = "_id:islrg_c,_fmt:html";
@ -1633,6 +1634,7 @@ class google{
);
}
}
}
return $out;
}

View File

@ -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];
}
}

View File

@ -58,6 +58,56 @@ $settings = [
[
"name" => "Scrapers to use",
"settings" => [
[
"description" => "Autocomplete<br><i>Picking <div class=\"code-inline\">Auto</div> changes the source dynamically depending of the page's scraper<br>Picking <div class=\"code-inline\">Disabled</div> disables this feature</i>",
"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
'<head>' .
'<meta http-equiv="Content-Type" content="text/html;charset=utf-8">' .
'<title>Settings</title>' .
'<link rel="stylesheet" href="/static/style.css?v3">' .
'<link rel="stylesheet" href="/static/style.css?v4">' .
'<meta name="viewport" content="width=device-width,initial-scale=1">' .
'<meta name="robots" content="index,follow">' .
'<link rel="icon" type="image/x-icon" href="/favicon.ico">' .
@ -260,14 +315,14 @@ $left =
'By clicking <div class="code-inline">Update settings!</div>, a plaintext <div class="code-inline">key=value</div> 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 .=
'<br><br>Your current cookie looks like this:' .
'<div class="code">';
$code = "";
$ca = 0;
foreach($_COOKIE as $key => $value){
@ -326,17 +381,23 @@ $left .=
'</div>' .
'<div class="settings-submit">' .
'<input type="submit" value="Update settings!">' .
'<a href="../">&lt; Return to main page</a>' .
'<a href="../">&lt; Return to front page</a>' .
'</div>' .
'</form>';
if(count($_GET) === 0){
echo
$frontend->load(
"search.html",
[
"class" => "",
"right-left" => "",
"right-left" =>
'<div class="infobox"><h2>Preference link</h2>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.<br><br>' .
'<a href="settings' . rtrim("?" . str_replace("; ", "&", $code), "?") . '">Bookmark me!</a>' .
'</div>',
"right-right" => "",
"left" => $left
]
);
}

View File

@ -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<highlight.length; k++){
// espace regex
regex.push(
highlight[k].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
);
}
regex = new RegExp(highlight.join("|"), "gi");
for(var i=0; i<list.length; i++){
html +=
'<div tabindex="0" class="entry" onclick="handle_entry_click(this);">' +
htmlspecialchars(
list[i]
).replace(
regex,
'<u>$&</u>'
) +
'</div>';
}
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();
});
}
}

View File

@ -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 */

View File

@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>{%title%}</title>
<link rel="stylesheet" href="/static/style.css?v3">
<link rel="stylesheet" href="/static/style.css?v4">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="{%index%}index,{%index%}follow">
<link rel="icon" type="image/x-icon" href="/favicon.ico">

View File

@ -4,7 +4,7 @@
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>4get</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/static/style.css?v3">
<link rel="stylesheet" href="/static/style.css?v4">
<meta name="robots" content="index,follow">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<meta name="description" content="4get.ca: They live in our walls!">
@ -33,6 +33,6 @@
Report a problem: <a href="https://lolcat.ca">lolcat.ca</a>
</div>
</div>
<script src="/static/client.js?v3"></script>
<script src="/static/client.js?v4"></script>
</body>
</html>