lolcat 2 jaren geleden
bovenliggende
commit
addc5a14a9
27 gewijzigde bestanden met toevoegingen van 1519 en 214 verwijderingen
  1. 21 10
      api.txt
  2. 20 0
      audio/linear.php
  3. 223 0
      audio/sc.php
  4. 20 0
      audio/seekable.php
  5. 214 0
      audio/spotify.php
  6. 91 35
      captcha.php
  7. 18 9
      data/config.php
  8. 3 2
      images.php
  9. 45 100
      lib/bot_protection.php
  10. BIN
      lib/classic.png
  11. 11 2
      lib/frontend.php
  12. 16 9
      lib/fuckhtml.php
  13. 28 19
      music.php
  14. 3 2
      news.php
  15. 1 1
      opensearch.php
  16. 15 12
      scraper/mojeek.php
  17. 7 4
      scraper/sc.php
  18. 0 0
      scraper/spotify.json
  19. 726 0
      scraper/spotify.php
  20. 1 1
      settings.php
  21. 41 2
      static/style.css
  22. 5 0
      template/header.html
  23. 1 1
      template/home.html
  24. 1 0
      template/images.html
  25. 1 0
      template/search.html
  26. 3 2
      videos.php
  27. 4 3
      web.php

+ 21 - 10
api.txt

@@ -267,20 +267,23 @@
     Each entry under "song" contains a array index called "stream" that
     Each entry under "song" contains a array index called "stream" that
     looks like this ::
     looks like this ::
     
     
-        endpoint: audio_sc
+        endpoint: sc
         url: https://api-v2.soundcloud <...>
         url: https://api-v2.soundcloud <...>
     
     
     
     
-    When the endpoint is "audio_sc", you MUST use 4get's audio_sc
-    endpoint, for example, if you want an audio stream back. Otherwise,
-    you are free to handle the json+m3u8 crap yourself. If the endpoint
-    is equal to "audio", that URL SHOULD return a valid HTTP audio
-    stream, and using the "audio" endpoint becomes optional again.
+    When the endpoint is something else than "linear", you MUST use
+    the specified endpoint. Otherwise, you are free to handle that
+    json+m3u8 crap yourself. If the endpoint is equal to "linear", the
+    URL should return a valid HTTP audio stream. To access the endpoint,
+    you must add the following prefix in your request, like so:
+	
+        https://4get.ca/audio/<endpoint>?s=<url>
 
 
 
 
 + /favicon
 + /favicon
     Get the favicon for a website. The only parameter is "s", and must
     Get the favicon for a website. The only parameter is "s", and must
-    include the protocol.
+    include the protocol for fetching in case the favicon is not cached
+    yet.
     
     
     Example ::
     Example ::
         
         
@@ -313,14 +316,14 @@
     is set.
     is set.
 
 
 
 
-+ /audio
++ /audio/linear
     Get a proxied audio file. Does not support "Range" headers, as it's
     Get a proxied audio file. Does not support "Range" headers, as it's
-    only used to proxy small files.
+    only used to proxy small files (hence why it's called linear DUH)
     
     
     The parameter is "s" for the audio link.
     The parameter is "s" for the audio link.
 
 
 
 
-+ /audio_sc
++ /audio/sc
     Get a proxied audio file for SoundCloud. Does not support downloads
     Get a proxied audio file for SoundCloud. Does not support downloads
     trough WGET or CURL, since it returns 30kb~160kb "206 Partial
     trough WGET or CURL, since it returns 30kb~160kb "206 Partial
     Content" parts, due to technical limitations that comes with
     Content" parts, due to technical limitations that comes with
@@ -334,6 +337,14 @@
     does not support "normal" SoundCloud URLs at this time.
     does not support "normal" SoundCloud URLs at this time.
 
 
 
 
++ /audio/spotify
+    Get a proxied Spotify audio file. Accepts a track ID for the "s"
+    parameter. Will only allow you to fetch the 30 second preview since
+    I don't feel like fucking with cookies and accounts every fucking
+    living moment of my life. You must handle the initial 302 redirect
+    to the /audio/linear endpoint.
+
+
 + Appendix
 + Appendix
     If you have any questions or need clarifications, please send an
     If you have any questions or need clarifications, please send an
     email my way to will at lolcat.ca
     email my way to will at lolcat.ca

+ 20 - 0
audio/linear.php

@@ -0,0 +1,20 @@
+<?php
+
+if(!isset($_GET["s"])){
+	
+	http_response_code(404);
+	header("X-Error: No SOUND(s) provided!");
+	die();
+}
+
+include "../data/config.php";
+include "../lib/curlproxy.php";
+$proxy = new proxy();
+
+try{
+	
+	$proxy->stream_linear_audio($_GET["s"]);
+}catch(Exception $error){
+	
+	header("X-Error: " . $error->getMessage());
+}

+ 223 - 0
audio/sc.php

@@ -0,0 +1,223 @@
+<?php
+
+new sc_audio();
+
+class sc_audio{
+	
+	public function __construct(){
+		
+		include "../lib/curlproxy.php";
+		$this->proxy = new proxy();
+		
+		if(isset($_GET["u"])){
+			
+			/*
+				we're now proxying audio
+			*/
+			$viewkey = $_GET["u"];
+			
+			if(!isset($_GET["r"])){
+				
+				$this->do404("Ranges(r) are missing");
+			}
+			
+			$ranges = explode(",", $_GET["r"]);
+			
+			// sanitize ranges
+			foreach($ranges as &$range){
+				
+				if(!is_numeric($range)){
+					
+					$this->do404("Invalid range specified");
+				}
+				
+				$range = (int)$range;
+			}
+			
+			// sort ranges (just to make sure)
+			sort($ranges);
+			
+			// convert ranges to pairs
+			$last = -1;
+			foreach($ranges as &$r){
+				
+				$tmp = $r;
+				$r = [$last + 1, $r];
+				
+				$last = $tmp;
+			}
+			
+			$browser_headers = getallheaders();
+			
+			// get the requested range from client
+			$client_range = 0;
+			foreach($browser_headers as $key => $value){
+				
+				if(strtolower($key) == "range"){
+					
+					preg_match(
+						'/bytes=([0-9]+)/',
+						$value,
+						$client_regex
+					);
+					
+					if(isset($client_regex[1])){
+						
+						$client_range = (int)$client_regex[1];
+					}else{
+						
+						$client_range = 0;
+					}
+					break;
+				}
+			}
+			
+			if(
+				$client_range < 0 ||
+				$client_range > $ranges[count($ranges) - 1][1]
+			){
+				
+				// range is not satisfiable
+				http_response_code(416);
+				header("Content-Type: text/plain");
+				die();
+			}
+			
+			$rng = null;
+			for($i=0; $i<count($ranges); $i++){
+				
+				if($ranges[$i][0] <= $client_range){
+					
+					$rng = $ranges[$i];
+				}
+			}
+			
+			// proxy data!
+			http_response_code(206); // partial content
+			header("Accept-Ranges: bytes");
+			header("Content-Range: bytes {$rng[0]}-{$rng[1]}/" . ($ranges[count($ranges) - 1][1] + 1));
+			
+			$viewkey =
+				preg_replace(
+					'/\/media\/([0-9]+)\/[0-9]+\/[0-9]+/',
+					'/media/$1/' . $rng[0] . '/' . $rng[1],
+					$viewkey
+				);
+			
+			try{
+				
+				$this->proxy->stream_linear_audio(
+					$viewkey
+				);
+			}catch(Exception $error){
+				
+				$this->do404("Could not read stream");
+			}
+			
+			die();
+		}
+		
+		/*
+			redirect user to correct resource
+			we need to scrape and store the byte positions in the result URL
+		*/
+		if(!isset($_GET["s"])){
+			
+			$this->do404("The URL(s) parameter is missing");
+		}
+		
+		$viewkey = $_GET["s"];
+		
+		if(
+			preg_match(
+				'/soundcloud\.com$/',
+				parse_url($viewkey, PHP_URL_HOST)
+			) === false
+		){
+			
+			$this->do404("This endpoint can only be used for soundcloud streams");
+		}
+		
+		try{
+			
+			$json = $this->proxy->get($viewkey)["body"];
+		}catch(Exception $error){
+			
+			$this->do404("Curl error: " . $error->getMessage());
+		}
+		
+		$json = json_decode($json, true);
+		
+		if(!isset($json["url"])){
+			
+			$this->do404("Could not get URL from JSON");
+		}
+		
+		$viewkey = $json["url"];
+		
+		$m3u8 = $this->proxy->get($viewkey)["body"];
+		
+		$m3u8 = explode("\n", $m3u8);
+		
+		$lineout = null;
+		$streampos_arr = [];
+		foreach($m3u8 as $line){
+			
+			$line = trim($line);
+			if($line[0] == "#"){
+				
+				continue;
+			}
+			
+			if($lineout === null){
+				$lineout = $line;
+			}
+			
+			preg_match(
+				'/\/media\/[0-9]+\/([0-9]+)\/([0-9]+)/',
+				$line,
+				$matches
+			);
+			
+			if(isset($matches[0])){
+				
+				$streampos_arr[] = [
+					(int)$matches[1],
+					(int)$matches[2]
+				];
+			}
+		}
+		
+		if($lineout === null){
+			
+			$this->do404("Could not get stream URL");
+		}
+		
+		$lineout =
+			preg_replace(
+				'/\/media\/([0-9]+)\/[0-9]+\/[0-9]+/',
+				'/media/$1/0/0',
+				$lineout
+			);
+		
+		$streampos = [];
+		
+		foreach($streampos_arr as $pos){
+			
+			$streampos[] = $pos[1];
+		}
+		
+		$streampos = implode(",", $streampos);
+		
+		header("Location: /audio/sc?u=" . urlencode($lineout) . "&r=$streampos");
+		header("Accept-Ranges: bytes");
+	}
+	
+	private function do404($error){
+		
+		http_response_code(404);
+		header("Content-Type: text/plain");
+		header("X-Error: $error");
+		die();
+	}
+}

+ 20 - 0
audio/seekable.php

@@ -0,0 +1,20 @@
+<?php
+
+if(!isset($_GET["s"])){
+	
+	http_response_code(404);
+	header("X-Error: No SOUND(s) provided!");
+	die();
+}
+
+include "../data/config.php";
+include "../lib/curlproxy.php";
+$proxy = new proxy();
+
+try{
+	
+	$proxy->stream_linear_audio($_GET["s"]);
+}catch(Exception $error){
+	
+	header("X-Error: " . $error->getMessage());
+}

+ 214 - 0
audio/spotify.php

@@ -0,0 +1,214 @@
+<?php
+
+include "../data/config.php";
+new spotify();
+
+class spotify{
+	
+	public function __construct(){
+		
+		include "../lib/fuckhtml.php";
+		$this->fuckhtml = new fuckhtml();
+		
+		if(
+			!isset($_GET["s"]) ||
+			!preg_match(
+				'/^(track|episode)\.([A-Za-z0-9]{22})$/',
+				$_GET["s"],
+				$matches
+			)
+		){
+			
+			$this->do404("The track ID(s) parameter is missing or invalid");
+		}
+		
+		try{
+			
+			if($matches[1] == "episode"){
+				
+				$uri = "show";
+			}else{
+				
+				$uri = $matches[1];
+			}
+			
+			$embed =
+				$this->get("https://embed.spotify.com/{$uri}/" . $matches[2]);
+		}catch(Exception $error){
+			
+			$this->do404("Failed to fetch embed data");
+		}
+		
+		$this->fuckhtml->load($embed);
+		
+		$json =
+			$this->fuckhtml
+			->getElementById(
+				"__NEXT_DATA__",
+				"script"
+			);
+		
+		if($json === null){
+			
+			$this->do404("Failed to extract JSON");
+		}
+		
+		$json =
+			json_decode($json["innerHTML"], true);
+		
+		if($json === null){
+			
+			$this->do404("Failed to decode JSON");
+		}
+		
+		switch($matches[1]){
+			
+			case "track":
+				if(
+					isset(
+						$json
+						["props"]
+						["pageProps"]
+						["state"]
+						["data"]
+						["entity"]
+						["audioPreview"]
+						["url"]
+					)
+				){
+					
+					header("Content-type: audio/mpeg");
+					header(
+						"Location: /audio/linear?s=" .
+						urlencode(
+							$json
+							["props"]
+							["pageProps"]
+							["state"]
+							["data"]
+							["entity"]
+							["audioPreview"]
+							["url"]
+						)
+					);
+				}else{
+					
+					$this->do404("Could not extract playback URL");
+				}
+				break;
+			
+			case "episode":
+				if(
+					isset(
+						$json
+						["props"]
+						["pageProps"]
+						["state"]
+						["data"]
+						["entity"]
+						["id"]
+					)
+				){
+					
+					try{
+						$json =
+							$this->get(
+								"https://spclient.wg.spotify.com/soundfinder/v1/unauth/episode/" .
+								$json
+								["props"]
+								["pageProps"]
+								["state"]
+								["data"]
+								["entity"]
+								["id"] .
+								"/com.widevine.alpha"
+							);
+					}catch(Exception $error){
+						
+						$this->do404("Failed to fetch audio resource");
+					}
+					
+					$json = json_decode($json, true);
+					
+					if($json === null){
+						
+						$this->do404("Failed to decode audio resource JSON");
+					}
+					
+					if(
+						isset($json["passthrough"]) &&
+						$json["passthrough"] == "ALLOWED" &&
+						isset($json["passthroughUrl"])
+					){
+						
+						header(
+							"Location:" .
+							"/audio/linear.php?s=" .
+							urlencode(
+								str_replace(
+									"http://",
+									"https://",
+									$json["passthroughUrl"]
+								)
+							)
+						);
+					}else{
+						
+						$this->do404("Failed to find passthroughUrl");
+					}
+					
+				}else{
+					
+					$this->do404("Failed to find episode ID");
+				}
+				break;
+			}
+	}
+	
+	private function get($url){
+		
+		$headers = [
+			"User-Agent: " . config::USER_AGENT,
+			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+			"Accept-Language: en-US,en;q=0.5",
+			"Accept-Encoding: gzip",
+			"DNT: 1",
+			"Connection: keep-alive",
+			"Upgrade-Insecure-Requests: 1",
+			"Sec-Fetch-Dest: document",
+			"Sec-Fetch-Mode: navigate",
+			"Sec-Fetch-Site: none",
+			"Sec-Fetch-User: ?1"
+		];
+		
+		$curlproc = curl_init();
+		
+		curl_setopt($curlproc, CURLOPT_URL, $url);
+		
+		curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
+		curl_setopt($curlproc, CURLOPT_HTTPHEADER, $headers);
+		
+		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){
+		
+		http_response_code(404);
+		header("Content-Type: text/plain");
+		header("X-Error: $error");
+		die();
+	}
+}

+ 91 - 35
captcha.php

@@ -1,47 +1,104 @@
 <?php
 <?php
 
 
 if(
 if(
-	!isset($_GET["k"]) ||
+	isset($_GET["v"]) === false ||
+	is_array($_GET["v"]) === true ||
 	preg_match(
 	preg_match(
-		'/^c\.[0-9]+$/',
-		$_GET["k"]
-	)
+		'/^c[0-9]+\.[A-Za-z0-9_]{20}$/',
+		$_GET["v"]
+	) === 0
 ){
 ){
 	
 	
+	http_response_code(401);
 	header("Content-Type: text/plain");
 	header("Content-Type: text/plain");
-	echo "Fuck you";
+	echo "Fuck my feathered cloaca";
 	die();
 	die();
 }
 }
 
 
-header("Content-Type: image/jpeg");
+//header("Content-Type: image/jpeg");
+include "data/config.php";
+
+if(config::BOT_PROTECTION !== 1){
+	
+	header("Content-Type: text/plain");
+	echo "The IQ test is disabled";
+	die();
+}
 
 
-$grid = apcu_fetch($_GET["k"]);
+$grid = apcu_fetch($_GET["v"]);
 
 
-if(
-	$grid === false ||
-	$grid[3] === true // has already been generated
-){
+if($grid !== false){
 	
 	
+	// captcha already generated
 	http_response_code(304); // not modified
 	http_response_code(304); // not modified
 	die();
 	die();
 }
 }
 
 
+header("Content-Type: image/jpeg");
 header("Last-Modified: Thu, 01 Oct 1970 00:00:00 GMT");
 header("Last-Modified: Thu, 01 Oct 1970 00:00:00 GMT");
 
 
-// only generate one captcha with this config
+// ** generate captcha data
+// get the positions for the answers
+// will return between 3 and 6 answer positions
+$range = range(0, 15);
+$answer_pos = [];
+
+array_splice($range, 0, 1);
+
+$picks = random_int(3, 6);
+
+for($i=0; $i<$picks; $i++){
+	
+	$answer_pos_tmp =
+		array_splice(
+			$range,
+			random_int(
+				0,
+				14 - $i
+			),
+			1
+		);
+	
+	$answer_pos[] = $answer_pos_tmp[0];
+}
+
+// choose a dataset
+$c = count(config::CAPTCHA_DATASET);
+$choosen = config::CAPTCHA_DATASET[random_int(0, $c - 1)];
+$choices = [];
+
+for($i=0; $i<$c; $i++){
+	
+	if(config::CAPTCHA_DATASET[$i][0] == $choosen[0]){
+		
+		continue;
+	}
+	
+	$choices[] = config::CAPTCHA_DATASET[$i];
+}
+
+// generate grid data
+$grid = [];
+
+for($i=0; $i<16; $i++){
+	
+	if(in_array($i, $answer_pos)){
+		
+		$grid[] = $choosen;
+	}else{
+		
+		$grid[] = $choices[random_int(0, count($choices) - 1)];
+	}
+}
+
+// store grid data for form validation on captcha_gen.php
 apcu_store(
 apcu_store(
-	$_GET["k"],
-	[
-		$grid[0],
-		$grid[1],
-		$grid[2],
-		true // has captcha been generated?
-	],
-	120 // we give user another 2 minutes to solve
+	$_GET["v"],
+	$answer_pos,
+	60 // we give user 1 minute to solve
 );
 );
 
 
 // generate image
 // generate image
-
 if(random_int(0,1) === 0){
 if(random_int(0,1) === 0){
 	
 	
 	$theme = [
 	$theme = [
@@ -57,7 +114,7 @@ if(random_int(0,1) === 0){
 }
 }
 
 
 $im = new Imagick();
 $im = new Imagick();
-$im->newImage(400, 400, $theme["bg"]);
+$im->newImage(400, 427, $theme["bg"]);
 $im->setImageBackgroundColor($theme["bg"]);
 $im->setImageBackgroundColor($theme["bg"]);
 $im->setImageFormat("jpg");
 $im->setImageFormat("jpg");
 
 
@@ -76,12 +133,18 @@ for($y=0; $y<4; $y++){
 	
 	
 	for($x=0; $x<4; $x++){
 	for($x=0; $x<4; $x++){
 		
 		
-		$tmp = new Imagick("./data/captcha/" . $grid[0][$i][0] . "/" . random_int(1, $grid[0][$i][1]) . ".png");
+		$tmp = new Imagick("./data/captcha/" . $grid[$i][0] . "/" . random_int(1, $grid[$i][1]) . ".png");
 		
 		
 		// convert transparency correctly
 		// convert transparency correctly
 		$tmp->setImageBackgroundColor("black");
 		$tmp->setImageBackgroundColor("black");
 		$tmp->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
 		$tmp->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
-
+		
+		// randomly mirror
+		if(random_int(0,1) === 1){
+			
+			$tmp->flopImage();
+		}
+		
 		// distort $tmp
 		// distort $tmp
 		$tmp->distortImage(
 		$tmp->distortImage(
 			$distort[random_int(0,1)],
 			$distort[random_int(0,1)],
@@ -101,21 +164,15 @@ for($y=0; $y<4; $y++){
 			false
 			false
 		);
 		);
 		
 		
+		$tmp->addNoiseImage($noise[random_int(0, 1)]);
+		
 		// append image
 		// append image
-		$im->compositeImage($tmp->getImage(), Imagick::COMPOSITE_DEFAULT, $x * 100, $y * 100);
+		$im->compositeImage($tmp->getImage(), Imagick::COMPOSITE_DEFAULT, $x * 100, ($y * 100) + 27);
 		
 		
 		$i++;
 		$i++;
 	}
 	}
 }
 }
 
 
-// add noise
-$im->addNoiseImage($noise[random_int(0, 1)]);
-
-// expand top of image
-$im->setImageGravity(Imagick::GRAVITY_SOUTH);
-$im->chopImage(0, -27, 400, 400);
-$im->extentImage(0, 0, 0, -27);
-
 // add text
 // add text
 $draw = new ImagickDraw();
 $draw = new ImagickDraw();
 $draw->setFontSize(20);
 $draw->setFontSize(20);
@@ -123,7 +180,7 @@ $draw->setFillColor($theme["fg"]);
 //$draw->setTextAntialias(false);
 //$draw->setTextAntialias(false);
 $draw->setFont("./data/captcha/font.ttf");
 $draw->setFont("./data/captcha/font.ttf");
 
 
-$text = "Pick " . $grid[1] . " images of " . str_replace("_", " ", $grid[2]);
+$text = "Pick " . $picks . " images of " . str_replace("_", " ", $choosen[0]);
 
 
 $pos = 200 - ($im->queryFontMetrics($draw, $text)["textWidth"] / 2);
 $pos = 200 - ($im->queryFontMetrics($draw, $text)["textWidth"] / 2);
 
 
@@ -143,5 +200,4 @@ for($i=0; $i<strlen($text); $i++){
 
 
 $im->setFormat("jpeg");
 $im->setFormat("jpeg");
 $im->setImageCompressionQuality(90);
 $im->setImageCompressionQuality(90);
-$im->setImageCompression(Imagick::COMPRESSION_JPEG2000);
 echo $im->getImageBlob();
 echo $im->getImageBlob();

+ 18 - 9
data/config.php

@@ -5,7 +5,7 @@ class config{
 	// any parameters.
 	// any parameters.
 	
 	
 	// 4get version. Please keep this updated
 	// 4get version. Please keep this updated
-	const VERSION = 6;
+	const VERSION = 7;
 	
 	
 	// Will be shown pretty much everywhere.
 	// Will be shown pretty much everywhere.
 	const SERVER_NAME = "4get";
 	const SERVER_NAME = "4get";
@@ -24,10 +24,10 @@ class config{
 	const API_ENABLED = true;
 	const API_ENABLED = true;
 	
 	
 	// Bot protection
 	// Bot protection
-	// 4get.ca has been hit with 250k bot reqs every single day for months
+	// 4get.ca has been hit with 500k bot reqs every single day for months
 	// you probably want to enable this if your instance is public...
 	// you probably want to enable this if your instance is public...
 	// 0 = disabled
 	// 0 = disabled
-	// 1 = ask for image captcha (requires image dataset & imagick 6.9.11-60)
+	// 1 = ask for image captcha (requires imagemagick v6 or higher)
 	// @TODO: 2 = invite only (users needs a pass)
 	// @TODO: 2 = invite only (users needs a pass)
 	const BOT_PROTECTION = 0;
 	const BOT_PROTECTION = 0;
 	
 	
@@ -62,20 +62,27 @@ class config{
 		"https://4get.zzls.xyz",
 		"https://4get.zzls.xyz",
 		"https://4getus.zzls.xyz",
 		"https://4getus.zzls.xyz",
 		"https://4get.silly.computer",
 		"https://4get.silly.computer",
-		"https://4g.opnxng.com",
 		"https://4get.konakona.moe",
 		"https://4get.konakona.moe",
 		"https://4get.lvkaszus.pl",
 		"https://4get.lvkaszus.pl",
 		"https://4g.ggtyler.dev",
 		"https://4g.ggtyler.dev",
 		"https://4get.perennialte.ch",
 		"https://4get.perennialte.ch",
-		"https://4get.sihj.net",
+		"https://4get.sijh.net",
 		"https://4get.hbubli.cc",
 		"https://4get.hbubli.cc",
 		"https://4get.plunked.party",
 		"https://4get.plunked.party",
-		"https://4get.seitan-ayoub.lol"
+		"https://4get.seitan-ayoub.lol",
+		"https://4get.etenie.pl",
+		"https://4get.lunar.icu",
+		"https://4get.dcs0.hu",
+		"https://4get.kizuki.lol",
+		"https://4get.psily.garden",
+		"https://search.milivojevic.in.rs",
+		"https://4get.snine.nl",
+		"https://4get.datura.network"
 	];
 	];
 	
 	
 	// Default user agent to use for scraper requests. Sometimes ignored to get specific webpages
 	// Default user agent to use for scraper requests. Sometimes ignored to get specific webpages
 	// Changing this might break things.
 	// Changing this might break things.
-	const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/120.0";
+	const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0";
 	
 	
 	// Proxy pool assignments for each scraper
 	// Proxy pool assignments for each scraper
 	// false = Use server's raw IP
 	// false = Use server's raw IP
@@ -94,6 +101,8 @@ class config{
 	const PROXY_YT = false; // youtube
 	const PROXY_YT = false; // youtube
 	const PROXY_YEP = false;
 	const PROXY_YEP = false;
 	const PROXY_PINTEREST = false;
 	const PROXY_PINTEREST = false;
+	const PROXY_SEZNAM = false;
+	const PROXY_NAVER = false;
 	const PROXY_FTM = false; // findthatmeme
 	const PROXY_FTM = false; // findthatmeme
 	const PROXY_IMGUR = false;
 	const PROXY_IMGUR = false;
 	const PROXY_YANDEX_W = false; // yandex web
 	const PROXY_YANDEX_W = false; // yandex web
@@ -107,8 +116,8 @@ class config{
 	// SOUNDCLOUD
 	// SOUNDCLOUD
 	// Get these parameters by making a search on soundcloud with network
 	// Get these parameters by making a search on soundcloud with network
 	// tab open, then filter URLs using "search?q=". (No need to login)
 	// tab open, then filter URLs using "search?q=". (No need to login)
-	const SC_USER_ID = "361066-632137-891392-693457";
-	const SC_CLIENT_TOKEN = "nUB9ZvnjRiqKF43CkKf3iu69D8bboyKY";
+	const SC_USER_ID = "59333-426459-717969-168008";
+	const SC_CLIENT_TOKEN = "8BBZpqUP1KSN4W6YB64xog2PX4Dw98b1";
 	
 	
 	// MARGINALIA
 	// MARGINALIA
 	// Get an API key by contacting the Marginalia.nu maintainer. The "public" key
 	// Get an API key by contacting the Marginalia.nu maintainer. The "public" key

+ 3 - 2
images.php

@@ -15,10 +15,11 @@ $get = $frontend->parsegetfilters($_GET, $filters);
 /*
 /*
 	Captcha
 	Captcha
 */
 */
-include "lib/captcha_gen.php";
-new captcha($frontend, $get, $filters, "images", true);
+include "lib/bot_protection.php";
+new bot_protection($frontend, $get, $filters, "images", true);
 
 
 $payload = [
 $payload = [
+	"timetaken" => microtime(true),
 	"images" => "",
 	"images" => "",
 	"nextpage" => ""
 	"nextpage" => ""
 ];
 ];

+ 45 - 100
lib/captcha_gen.php → lib/bot_protection.php

@@ -1,6 +1,6 @@
 <?php
 <?php
 
 
-class captcha{
+class bot_protection{
 	
 	
 	public function __construct($frontend, $get, $filters, $page, $output){
 	public function __construct($frontend, $get, $filters, $page, $output){
 		
 		
@@ -26,7 +26,7 @@ class captcha{
 			if(
 			if(
 				// check if key is not malformed
 				// check if key is not malformed
 				preg_match(
 				preg_match(
-					'/^c[0-9]+\.[A-Za-z0-9]{20}$/',
+					'/^k[0-9]+\.[A-Za-z0-9_]{20}$/',
 					$_COOKIE["pass"]
 					$_COOKIE["pass"]
 				) &&
 				) &&
 				// does key exist
 				// does key exist
@@ -39,7 +39,7 @@ class captcha{
 				// we start counting from 1
 				// we start counting from 1
 				// when it has been incremented to 102, it has reached
 				// when it has been incremented to 102, it has reached
 				// 100 reqs
 				// 100 reqs
-				if($inc >= 102){
+				if($inc >= config::MAX_SEARCHES + 2){
 					
 					
 					// reached limit, delete and give captcha
 					// reached limit, delete and give captcha
 					apcu_delete($_COOKIE["pass"]);
 					apcu_delete($_COOKIE["pass"]);
@@ -62,7 +62,7 @@ class captcha{
 		
 		
 		if($output === false){
 		if($output === false){
 			
 			
-			http_response_code(429); // too many reqs
+			http_response_code(401); // forbidden
 			echo json_encode([
 			echo json_encode([
 				"status" => "The \"pass\" token in your cookies is missing or has expired!!"
 				"status" => "The \"pass\" token in your cookies is missing or has expired!!"
 			]);
 			]);
@@ -104,10 +104,13 @@ class captcha{
 				!isset($regex[0][1])
 				!isset($regex[0][1])
 			){
 			){
 				
 				
-				// check if its k
+				// check if its the v key
 				if(
 				if(
-					$line[0] == "k" &&
-					strpos($line[1], "c.") === 0
+					$line[0] == "v" &&
+					preg_match(
+						'/^c[0-9]+\.[A-Za-z0-9_]{20}$/',
+						$line[1]
+					)
 				){
 				){
 					
 					
 					$key = apcu_fetch($line[1]);
 					$key = apcu_fetch($line[1]);
@@ -129,27 +132,21 @@ class captcha{
 			
 			
 			$answers[] = $regex;
 			$answers[] = $regex;
 		}
 		}
-
+		
 		if(
 		if(
 			!$invalid &&
 			!$invalid &&
-			$key !== false
+			$key !== false // has captcha been gen'd?
 		){
 		){
-			$check = $key[1];
+			$check = count($key);
 			
 			
 			// validate answer
 			// validate answer
-			for($i=0; $i<count($key[0]); $i++){
-				
-				if(!in_array($i, $answers)){
-					
-					continue;
-				}
+			for($i=0; $i<count($answers); $i++){
 				
 				
-				if($key[0][$i][0] == $key[2]){
+				if(in_array($answers[$i], $key)){
 					
 					
 					$check--;
 					$check--;
 				}else{
 				}else{
 					
 					
-					// got a wrong answer
 					$check = -1;
 					$check = -1;
 					break;
 					break;
 				}
 				}
@@ -160,21 +157,8 @@ class captcha{
 				// we passed the captcha
 				// we passed the captcha
 				// set cookie
 				// set cookie
 				$inc = apcu_inc("cookie");
 				$inc = apcu_inc("cookie");
-				$chars =
-					array_merge(
-						range("A", "Z"),
-						range("a", "z"),
-						range(0, 9)
-					);
-				
-				$c = count($chars) - 1;
 				
 				
-				$key = "c" . $inc . ".";
-				
-				for($i=0; $i<20; $i++){
-					
-					$key .= $chars[random_int(0, $c)];
-				}
+				$key = "k" . $inc . "." . $this->randomchars();
 				
 				
 				apcu_inc($key, 1, $stupid, 86400);
 				apcu_inc($key, 1, $stupid, 86400);
 				
 				
@@ -203,84 +187,23 @@ class captcha{
 			}
 			}
 		}
 		}
 		
 		
-		// get the positions for the answers
-		// will return between 3 and 6 answer positions
-		$range = range(0, 15);
-		$answer_pos = [];
-
-		array_splice($range, 0, 1);
-
-		for($i=0; $i<random_int(3, 6); $i++){
-			
-			$answer_pos_tmp =
-				array_splice(
-					$range,
-					random_int(
-						0,
-						14 - $i
-					),
-					1
-				);
-			
-			$answer_pos[] = $answer_pos_tmp[0];
-		}
-
-		// choose a dataset
-		$c = count(config::CAPTCHA_DATASET);
-		$choosen = config::CAPTCHA_DATASET[random_int(0, $c - 1)];
-		$choices = [];
-
-		for($i=0; $i<$c; $i++){
-			
-			if(config::CAPTCHA_DATASET[$i][0] == $choosen[0]){
-				
-				continue;
-			}
-			
-			$choices[] = config::CAPTCHA_DATASET[$i];
-		}
-
-		// generate grid data
-		$grid = [];
-
-		for($i=0; $i<16; $i++){
-			
-			if(in_array($i, $answer_pos)){
-				
-				$grid[] = $choosen;
-			}else{
-				
-				$grid[] = $choices[random_int(0, count($choices) - 1)];
-			}
-		}
-
-		$key = "c." . apcu_inc("captcha_gen", 1) . "." . random_int(0, 100000000);
-
-		apcu_store(
-			$key,
-			[
-				$grid,
-				count($answer_pos),
-				$choosen[0],
-				false // has captcha been generated?
-			],
-			120 // we give user 2 minutes to get captcha, in case of network error
-		);
+		$key = "c" . apcu_inc("captcha_gen", 1) . "." . $this->randomchars();
 		
 		
 		$payload = [
 		$payload = [
+			"timetaken" => microtime(true),
 			"class" => "",
 			"class" => "",
 			"right-left" => "",
 			"right-left" => "",
 			"right-right" => "",
 			"right-right" => "",
 			"left" =>
 			"left" =>
 				'<div class="infobox">' .
 				'<div class="infobox">' .
 					'<h1>IQ test</h1>' .
 					'<h1>IQ test</h1>' .
-					'Due to getting hit with 20,000 bot requests per day, I had to put this up. Sorry.<br><br>' .
-					'Solving this captcha will allow you to make 100 searches today. I will add a way for legit users to bypass the captcha later. Sorry /g/tards!!' .
+					'IQ test has been enabled due to bot abuse on the network.<br>' .
+					'Solving this IQ test will let you make 100 searches today. I will add an invite system to bypass this soon...' .
 					$error .
 					$error .
 					'<form method="POST" enctype="text/plain" autocomplete="off">' .
 					'<form method="POST" enctype="text/plain" autocomplete="off">' .
 						'<div class="captcha-wrapper">' .
 						'<div class="captcha-wrapper">' .
 							'<div class="captcha">' .
 							'<div class="captcha">' .
-								'<img src="captcha?k=' . $key . '" alt="Captcha image">' .
+								'<img src="captcha.php?v=' . $key . '" alt="Captcha image">' .
 								'<div class="captcha-controls">' .
 								'<div class="captcha-controls">' .
 									'<input type="checkbox" name="c[0]" id="c0">' .
 									'<input type="checkbox" name="c[0]" id="c0">' .
 									'<label for="c0"></label>' .
 									'<label for="c0"></label>' .
@@ -317,13 +240,12 @@ class captcha{
 								'</div>' .
 								'</div>' .
 							'</div>' .
 							'</div>' .
 						'</div>' .
 						'</div>' .
-						'<input type="hidden" name="k" value="' . $key . '">' .
+						'<input type="hidden" name="v" value="' . $key . '">' .
 						'<input type="submit" value="Check IQ" class="captcha-submit">' .
 						'<input type="submit" value="Check IQ" class="captcha-submit">' .
 					'</form>' .
 					'</form>' .
 				'</div>'
 				'</div>'
 		];
 		];
 		
 		
-		http_response_code(429); // too many reqs
 		$frontend->loadheader(
 		$frontend->loadheader(
 			$get,
 			$get,
 			$filters,
 			$filters,
@@ -333,4 +255,27 @@ class captcha{
 		echo $frontend->load("search.html", $payload);
 		echo $frontend->load("search.html", $payload);
 		die();
 		die();
 	}
 	}
+	
+	private function randomchars(){
+		
+		$chars =
+			array_merge(
+				range("A", "Z"),
+				range("a", "z"),
+				range(0, 9)
+			);
+		
+		$chars[] = "_";
+		
+		$c = count($chars) - 1;
+		
+		$key = "";
+		
+		for($i=0; $i<20; $i++){
+			
+			$key .= $chars[random_int(0, $c)];
+		}
+		
+		return $key;
+	}
 }
 }

BIN
lib/classic.png


+ 11 - 2
lib/frontend.php

@@ -39,6 +39,14 @@ class frontend{
 			$replacements["ac"] = '';
 			$replacements["ac"] = '';
 		}
 		}
 		
 		
+		if(
+			isset($replacements["timetaken"]) &&
+			$replacements["timetaken"] !== null
+		){
+			
+			$replacements["timetaken"] = '<div class="timetaken">Took ' . substr(microtime(true) - $replacements["timetaken"], 0, 4) . 's</div>';
+		}
+		
 		$handle = fopen("template/{$template}", "r");
 		$handle = fopen("template/{$template}", "r");
 		$data = fread($handle, filesize("template/{$template}"));
 		$data = fread($handle, filesize("template/{$template}"));
 		fclose($handle);
 		fclose($handle);
@@ -68,7 +76,7 @@ class frontend{
 		
 		
 		echo
 		echo
 			$this->load("header.html", [
 			$this->load("header.html", [
-				"title" => trim($get["s"] . " ({$page})"),
+				"title" => trim(htmlspecialchars($get["s"]) . " ({$page})"),
 				"description" => ucfirst($page) . ' search results for &quot;' . htmlspecialchars($get["s"]) . '&quot;',
 				"description" => ucfirst($page) . ' search results for &quot;' . htmlspecialchars($get["s"]) . '&quot;',
 				"index" => "no",
 				"index" => "no",
 				"search" => htmlspecialchars($get["s"]),
 				"search" => htmlspecialchars($get["s"]),
@@ -88,7 +96,7 @@ class frontend{
 			
 			
 			$this->drawerror(
 			$this->drawerror(
 				"Tshh, blocked!",
 				"Tshh, blocked!",
-				'You were blocked from viewing this page. If you wish to scrape data from 4get, please consider running <a href="https://git.lolcat.ca/lolcat/4get" rel="noreferrer nofollow">your own 4get instance</a> or using <a href="/api.txt">the API</a>.',
+				'You were blocked from viewing this page. If you wish to scrape data from 4get, please consider running <a href="https://git.lolcat.ca/lolcat/4get" rel="noreferrer nofollow">your own 4get instance</a>.',
 			);
 			);
 			die();
 			die();
 		}
 		}
@@ -98,6 +106,7 @@ class frontend{
 		
 		
 		echo
 		echo
 			$this->load("search.html", [
 			$this->load("search.html", [
+				"timetaken" => null,
 				"class" => "",
 				"class" => "",
 				"right-left" => "",
 				"right-left" => "",
 				"right-right" => "",
 				"right-right" => "",

+ 16 - 9
lib/fuckhtml.php

@@ -466,19 +466,26 @@ class fuckhtml{
 		
 		
 		return
 		return
 			preg_replace_callback(
 			preg_replace_callback(
-				'/\\\u[A-Fa-f0-9]{4}|\\\x[A-Fa-f0-9]{2}/',
+				'/\\\u[A-Fa-f0-9]{4}|\\\x[A-Fa-f0-9]{2}|\\\n|\\\r/',
 				function($match){
 				function($match){
 					
 					
-					if($match[0][1] == "u"){
+					switch($match[0][1]){
 						
 						
-						return json_decode('"' . $match[0] . '"');
-					}else{
+						case "u":
+							return json_decode('"' . $match[0] . '"');
+							break;
 						
 						
-						return mb_convert_encoding(
-							stripcslashes($match[0]),
-							"utf-8",
-							"windows-1252"
-						);
+						case "x":
+							return mb_convert_encoding(
+								stripcslashes($match[0]),
+								"utf-8",
+								"windows-1252"
+							);
+							break;
+						
+						default:
+							return " ";
+							break;
 					}
 					}
 				},
 				},
 				$string
 				$string

+ 28 - 19
music.php

@@ -15,10 +15,11 @@ $get = $frontend->parsegetfilters($_GET, $filters);
 /*
 /*
 	Captcha
 	Captcha
 */
 */
-include "lib/captcha_gen.php";
-new captcha($frontend, $get, $filters, "music", true);
+include "lib/bot_protection.php";
+new bot_protection($frontend, $get, $filters, "music", true);
 
 
 $payload = [
 $payload = [
+	"timetaken" => microtime(true),
 	"class" => "",
 	"class" => "",
 	"right-left" => "",
 	"right-left" => "",
 	"right-right" => "",
 	"right-right" => "",
@@ -36,7 +37,10 @@ try{
 $categories = [
 $categories = [
 	"song" => "",
 	"song" => "",
 	"author" => "",
 	"author" => "",
-	"playlist" => ""
+	"playlist" => "",
+	"album" => "",
+	"podcast" => "",
+	"user" => ""
 ];
 ];
 
 
 /*
 /*
@@ -48,14 +52,26 @@ if(count($results["song"]) !== 0){
 	
 	
 	$main = "song";
 	$main = "song";
 	
 	
-}elseif(count($results["author"]) !== 0){
+}elseif(count($results["album"]) !== 0){
 	
 	
-	$main = "author";
+	$main = "album";
 	
 	
 }elseif(count($results["playlist"]) !== 0){
 }elseif(count($results["playlist"]) !== 0){
 	
 	
 	$main = "playlist";
 	$main = "playlist";
 	
 	
+}elseif(count($results["podcast"]) !== 0){
+	
+	$main = "podcast";
+
+}elseif(count($results["author"]) !== 0){
+	
+	$main = "author";
+		
+}elseif(count($results["user"]) !== 0){
+	
+	$main = "user";
+	
 }else{
 }else{
 	
 	
 	// No results found!
 	// No results found!
@@ -133,12 +149,15 @@ foreach($categories as $name => $data){
 		$customhtml = null;
 		$customhtml = null;
 		
 		
 		if(
 		if(
-			$name == "song" &&
+			(
+				$name == "song" ||
+				$name == "podcast"
+			) &&
 			$item["stream"]["endpoint"] !== null
 			$item["stream"]["endpoint"] !== null
 		){
 		){
 			
 			
 			$customhtml =
 			$customhtml =
-				'<audio src="' . $item["stream"]["endpoint"] . '?s=' . urlencode($item["stream"]["url"]) . '" controls autostart="false" preload="none">';
+				'<audio src="/audio/' . $item["stream"]["endpoint"] . '?s=' . urlencode($item["stream"]["url"]) . '" controls autostart="false" preload="none">';
 		}
 		}
 		
 		
 		$categories[$name] .= $frontend->drawtextresult($item, $greentext, $duration, $get["s"], $tabindex, $customhtml);
 		$categories[$name] .= $frontend->drawtextresult($item, $greentext, $duration, $get["s"], $tabindex, $customhtml);
@@ -177,18 +196,8 @@ foreach($categories as $name => $value){
 			'<div class="answer-title">' .
 			'<div class="answer-title">' .
 				'<a class="answer-title" href="?s=' . urlencode($get["s"]);
 				'<a class="answer-title" href="?s=' . urlencode($get["s"]);
 	
 	
-	switch($name){
-		
-		case "playlist":
-			$payload[$write] .=
-				'&type=playlist"><h2>Playlists</h2></a>';
-			break;
-		
-		case "author":
-			$payload[$write] .=
-				'&type=people"><h2>Authors</h2></a>';
-			break;
-	}
+	$payload[$write] .=
+		'&type=' . $name . '"><h2>' . ucfirst($name) . 's</h2></a>';
 	
 	
 	$payload[$write] .=
 	$payload[$write] .=
 			'</div>' .
 			'</div>' .

+ 3 - 2
news.php

@@ -15,10 +15,11 @@ $get = $frontend->parsegetfilters($_GET, $filters);
 /*
 /*
 	Captcha
 	Captcha
 */
 */
-include "lib/captcha_gen.php";
-new captcha($frontend, $get, $filters, "news", true);
+include "lib/bot_protection.php";
+new bot_protection($frontend, $get, $filters, "news", true);
 
 
 $payload = [
 $payload = [
+	"timetaken" => microtime(true),
 	"class" => "",
 	"class" => "",
 	"right-left" => "",
 	"right-left" => "",
 	"right-right" => "",
 	"right-right" => "",

+ 1 - 1
opensearch.php

@@ -5,7 +5,7 @@ include "data/config.php";
 
 
 $domain =
 $domain =
 	htmlspecialchars(
 	htmlspecialchars(
-		(strpos(strtolower($_SERVER['SERVER_PROTOCOL']), 'https') === false ? 'http' : 'https') .
+		((isset($_SERVER["HTTPS"]) && ($_SERVER["HTTPS"] == "on" || $_SERVER["HTTPS"] === 1)) ? "https" : "http") .
 		'://' . $_SERVER["HTTP_HOST"]
 		'://' . $_SERVER["HTTP_HOST"]
 	);
 	);
 
 

+ 15 - 12
scraper/mojeek.php

@@ -602,20 +602,23 @@ class mojeek{
 						);
 						);
 				}
 				}
 				
 				
-				$data["date"] =
-					explode(
-						" - ",
-						$this->fuckhtml
-						->getTextContent(
-							$this->fuckhtml
-							->getElementsByClassName("i", "p")[0]
-						)
+				$date =
+					$this->fuckhtml
+					->getElementsByClassName(
+						"mdate",
+						"span"
 					);
 					);
 				
 				
-				$data["date"] =
-					strtotime(
-						$data["date"][count($data["date"]) - 1]
-					);
+				if(count($date) !== 0){
+										
+					$data["date"] =
+						strtotime(
+							$this->fuckhtml
+							->getTextContent(
+								$date[0]
+							)
+						);
+				}
 				
 				
 				$out["web"][] = $data;
 				$out["web"][] = $data;
 			}
 			}

+ 7 - 4
scraper/sc.php

@@ -16,7 +16,7 @@ class sc{
 				"option" => [
 				"option" => [
 					"any" => "Any type",
 					"any" => "Any type",
 					"track" => "Tracks",
 					"track" => "Tracks",
-					"people" => "People",
+					"author" => "People",
 					"album" => "Albums",
 					"album" => "Albums",
 					"playlist" => "Playlists",
 					"playlist" => "Playlists",
 					"goplus" => "Go+ Tracks"
 					"goplus" => "Go+ Tracks"
@@ -143,7 +143,7 @@ class sc{
 					];
 					];
 					break;
 					break;
 				
 				
-				case "people":
+				case "author":
 					$url = "https://api-v2.soundcloud.com/search/users";
 					$url = "https://api-v2.soundcloud.com/search/users";
 					$params = [
 					$params = [
 						"q" => $search,
 						"q" => $search,
@@ -237,7 +237,10 @@ class sc{
 			"npt" => null,
 			"npt" => null,
 			"song" => [],
 			"song" => [],
 			"playlist" => [],
 			"playlist" => [],
-			"author" => []
+			"album" => [],
+			"podcast" => [],
+			"author" => [],
+			"user" => []
 		];
 		];
 		
 		
 		/*
 		/*
@@ -346,7 +349,7 @@ class sc{
 					if(stripos($item["monetization_model"], "TIER") === false){
 					if(stripos($item["monetization_model"], "TIER") === false){
 						
 						
 						$stream = [
 						$stream = [
-							"endpoint" => "audio_sc",
+							"endpoint" => "sc",
 							"url" =>
 							"url" =>
 								$item["media"]["transcodings"][0]["url"] .
 								$item["media"]["transcodings"][0]["url"] .
 								"?client_id=" . config::SC_CLIENT_TOKEN .
 								"?client_id=" . config::SC_CLIENT_TOKEN .

File diff suppressed because it is too large
+ 0 - 0
scraper/spotify.json


+ 726 - 0
scraper/spotify.php

@@ -0,0 +1,726 @@
+<?php
+
+class spotify{
+		
+	private const req_web = 0;
+	private const req_api = 1;
+	private const req_clientid = 2;
+	
+	public function __construct(){
+		
+		include "lib/backend.php";
+		$this->backend = new backend("spotify");
+		
+		include "lib/fuckhtml.php";
+		$this->fuckhtml = new fuckhtml();
+	}
+	
+	public function getfilters($page){
+		
+		return [
+			"category" => [
+				"display" => "Category",
+				"option" => [
+					"any" => "All (no pagination)",
+					"audiobooks" => "Audiobooks",
+					"tracks" => "Songs",
+					"artists" => "Artists",
+					"playlists" => "Playlists",
+					"albums" => "Albums",
+					"podcastAndEpisodes" => "Podcasts & Shows (no pagination)",
+					"episodes" => "Episodes",
+					"users" => "Profiles"
+				]
+			]
+		];
+	}
+	
+	private function get($proxy, $url, $get = [], $reqtype = self::req_web, $bearer = null, $token = null){
+		
+		$curlproc = curl_init();
+		
+		switch($reqtype){
+			
+			case self::req_api:
+				$headers = [
+					"User-Agent: " . config::USER_AGENT,
+					"Accept: application/json",
+					"Accept-Language: en",
+					"app-platform: WebPlayer",
+					"authorization: Bearer {$bearer}",
+					"client-token: {$token}",
+					"content-type: application/json;charset=UTF-8",
+					"Origin: https://open.spotify.com",
+					"Referer: https://open.spotify.com/",
+					"DNT: 1",
+					"Connection: keep-alive",
+					"Sec-Fetch-Dest: empty",
+					"Sec-Fetch-Mode: cors",
+					"Sec-Fetch-Site: same-site",
+					"spotify-app-version: 1.2.27.93.g7aee53d4",
+					"TE: trailers"
+				];
+				break;
+			
+			case self::req_web:
+				$headers = [
+					"User-Agent: " . config::USER_AGENT,
+					"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+					"Accept-Language: en-US,en;q=0.5",
+					"Accept-Encoding: gzip",
+					"DNT: 1",
+					"Sec-GPC: 1",
+					"Connection: keep-alive",
+					"Upgrade-Insecure-Requests: 1",
+					"Sec-Fetch-Dest: document",
+					"Sec-Fetch-Mode: navigate",
+					"Sec-Fetch-Site: cross-site"
+				];
+				break;
+			
+			case self::req_clientid:			
+				$get = json_encode($get);
+				
+				curl_setopt($curlproc, CURLOPT_POST, true);
+				curl_setopt($curlproc, CURLOPT_POSTFIELDS, $get);
+			
+				$headers = [
+					"User-Agent:" . config::USER_AGENT,
+					"Accept: application/json",
+					"Accept-Language: en-US,en;q=0.5",
+					"Accept-Encoding: gzip, deflate, br",
+					"Referer: https://open.spotify.com/",
+					"content-type: application/json",
+					"Content-Length: " . strlen($get),
+					"Origin: https://open.spotify.com",
+					"DNT: 1",
+					"Sec-GPC: 1",
+					"Connection: keep-alive",
+					"Sec-Fetch-Dest: empty",
+					"Sec-Fetch-Mode: cors",
+					"Sec-Fetch-Site: same-site",
+					"TE: trailers"
+				];
+				break;
+		}
+		
+		if($reqtype !== self::req_clientid){
+			if($get !== []){
+				$get = http_build_query($get);
+				$url .= "?" . $get;
+			}
+		}
+		
+		curl_setopt($curlproc, CURLOPT_URL, $url);
+		
+		curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
+		curl_setopt($curlproc, CURLOPT_HTTPHEADER, $headers);
+		
+		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);
+
+		$this->backend->assign_proxy($curlproc, $proxy);
+		
+		$data = curl_exec($curlproc);
+		
+		if(curl_errno($curlproc)){
+			throw new Exception(curl_error($curlproc));
+		}
+		
+		curl_close($curlproc);
+		return $data;
+	}
+	
+	public function music($get){
+		
+		$search = $get["s"];
+		$ip = $this->backend->get_ip();
+		$category = $get["category"];
+		
+		/*
+		audiobooks first and second page decoded
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchAudiobooks&variables={"searchTerm":"freddie+dredd","offset":0,"limit":30,"numberOfTopResults":20,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"8758e540afdba5afa3c5246817f6bd31d86a15b3f5666c363dd017030f35d785"}}
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchAudiobooks&variables={"searchTerm":"freddie+dredd","offset":30,"limit":30,"numberOfTopResults":20,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"8758e540afdba5afa3c5246817f6bd31d86a15b3f5666c363dd017030f35d785"}}
+		*/
+		
+		/*
+		songs
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchTracks&variables={"searchTerm":"asmr","offset":0,"limit":100,"numberOfTopResults":20,"includeAudiobooks":false}&extensions={"persistedQuery":{"version":1,"sha256Hash":"16c02d6304f5f721fc2eb39dacf2361a4543815112506a9c05c9e0bc9733a679"}}
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchTracks&variables={"searchTerm":"asmr","offset":100,"limit":100,"numberOfTopResults":20,"includeAudiobooks":false}&extensions={"persistedQuery":{"version":1,"sha256Hash":"16c02d6304f5f721fc2eb39dacf2361a4543815112506a9c05c9e0bc9733a679"}}
+		*/
+		
+		/*
+		artists
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchArtists&variables={"searchTerm":"asmr","offset":0,"limit":30,"numberOfTopResults":20,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"b8840daafdda9a9ceadb7c5774731f63f9eca100445d2d94665f2dc58b45e2b9"}}
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchArtists&variables={"searchTerm":"asmr","offset":30,"limit":23,"numberOfTopResults":20,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"b8840daafdda9a9ceadb7c5774731f63f9eca100445d2d94665f2dc58b45e2b9"}}
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchArtists&variables={"searchTerm":"asmr","offset":53,"limit":30,"numberOfTopResults":20,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"b8840daafdda9a9ceadb7c5774731f63f9eca100445d2d94665f2dc58b45e2b9"}}
+		*/
+		
+		/*
+		playlists
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchPlaylists&variables={"searchTerm":"asmr","offset":0,"limit":30,"numberOfTopResults":20,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"19b4143a0500ccec189ca0f4a0316bc2c615ecb51ce993ba4d7d08afd1d87aa4"}}
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchPlaylists&variables={"searchTerm":"asmr","offset":30,"limit":3,"numberOfTopResults":20,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"19b4143a0500ccec189ca0f4a0316bc2c615ecb51ce993ba4d7d08afd1d87aa4"}}
+		*/
+		
+		/*
+		albums
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchAlbums&variables={"searchTerm":"asmr","offset":33,"limit":30,"numberOfTopResults":20,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"e93b13cda461482da2940467eb2beed9368e9bb2fff37df3fb6633fc61271a27"}}
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchAlbums&variables={"searchTerm":"asmr","offset":33,"limit":30,"numberOfTopResults":20,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"e93b13cda461482da2940467eb2beed9368e9bb2fff37df3fb6633fc61271a27"}}
+		*/
+		
+		/*
+		podcasts & shows (contains authors, no pagination)
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchFullEpisodes&variables={"searchTerm":"asmr","offset":0,"limit":30}&extensions={"persistedQuery":{"version":1,"sha256Hash":"9f996251c9781fabce63f1a9980b5287ea33bc5e8c8953d0c4689b09936067a1"}}
+		*/
+		
+		/*
+		episodes
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchDesktop&variables={"searchTerm":"asmr","offset":0,"limit":10,"numberOfTopResults":5,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"da03293d92a2cfc5e24597dcdc652c0ad135e1c64a78fddbf1478a7e096bea44"}}
+		??? https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchFullEpisodes&variables={"searchTerm":"asmr","offset":60,"limit":30}&extensions={"persistedQuery":{"version":1,"sha256Hash":"9f996251c9781fabce63f1a9980b5287ea33bc5e8c8953d0c4689b09936067a1"}}
+		*/
+		
+		/*
+		profiles
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchUsers&variables={"searchTerm":"asmr","offset":0,"limit":30,"numberOfTopResults":20,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"02026f48ab5001894e598904079b620ebc64f2d53b55ca20c3858abd3a46c5fb"}}
+		https://api-partner.spotify.com/pathfinder/v1/query?operationName=searchUsers&variables={"searchTerm":"asmr","offset":30,"limit":30,"numberOfTopResults":20,"includeAudiobooks":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"02026f48ab5001894e598904079b620ebc64f2d53b55ca20c3858abd3a46c5fb"}}
+		*/
+		
+		// get HTML
+		try{
+			
+			$html =
+				$this->get(
+					$ip,
+					"https://open.spotify.com/search/" .
+					rawurlencode($search) .
+					($category != "any" ? "/" . $category : ""),
+					[]
+				);
+		}catch(Exception $error){
+			
+			throw new Exception("Failed to get initial search page");
+		}
+		
+		// grep bearer and client ID
+		$this->fuckhtml->load($html);
+		
+		$script =
+			$this->fuckhtml
+			->getElementById(
+				"session",
+				"script"
+			);
+		
+		if($script === null){
+			
+			throw new Exception("Failed to grep bearer token");
+		}
+		
+		$script =
+			json_decode(
+				$script["innerHTML"],
+				true
+			);
+		
+		$bearer = $script["accessToken"];
+		$client_id = $script["clientId"];
+		
+		// hit client ID endpoint
+		try{
+			
+			$token =
+				json_decode(
+					$this->get(
+						$ip,
+						"https://clienttoken.spotify.com/v1/clienttoken",
+						[ // !! that shit must be sent as json data
+							"client_data" => [
+								"client_id" => $client_id,
+								"client_version" => "1.2.27.93.g7aee53d4",
+								"js_sdk_data" => [
+									"device_brand" => "unknown",
+									"device_id" => "4c7ca20117ca12288ea8fc7118a9118c",
+									"device_model" => "unknown",
+									"device_name" => "computer",
+									"os" => "windows",
+									"os_version" => "NT 10.0"
+								]
+							]
+						],
+						self::req_clientid
+					),
+					true
+				);
+		}catch(Exception $error){
+			
+			throw new Exception("Failed to fetch token");
+		}
+		
+		if($token === null){
+			
+			throw new Exception("Failed to decode token");
+		}
+		
+		$token = $token["granted_token"]["token"];
+		
+		try{
+			
+			switch($get["option"]){
+				
+				case "any":
+					$variables = [
+						"searchTerm" => $search,
+						"offset" => 0,
+						"limit" => 10,
+						"numberOfTopResults" => 5,
+						"includeAudiobooks" => true
+					];
+					break;
+				
+				case "audiobooks":
+					
+					break;
+			}
+			
+			$payload =
+				$this->get(
+					$ip,
+					"https://api-partner.spotify.com/pathfinder/v1/query",
+					[
+						"operationName" => "searchDesktop",
+						"variables" =>
+							json_encode(
+								[
+									"searchTerm" => $search,
+									"offset" => 0,
+									"limit" => 10,
+									"numberOfTopResults" => 5,
+									"includeAudiobooks" => true
+								]
+							),
+						"extensions" =>
+							json_encode(
+								[
+									"persistedQuery" => [
+										"version" => 1,
+										"sha256Hash" => "21969b655b795601fb2d2204a4243188e75fdc6d3520e7b9cd3f4db2aff9591e" // ?
+									]
+								]
+							)
+					],
+					self::req_api,
+					$bearer,
+					$token
+				);
+			
+		}catch(Exception $error){
+			
+			throw new Exception("Failed to fetch JSON results");
+		}
+		
+		if($payload == "Token expired"){
+			
+			throw new Exception("Grepped spotify token has expired");
+		}
+		
+		$payload = json_decode($payload, true);
+		
+		if($payload === null){
+			
+			throw new Exception("Failed to decode JSON results");
+		}
+		
+		//$payload = json_decode(file_get_contents("scraper/spotify.json"), true);
+		
+		$out = [
+			"status" => "ok",
+			"npt" => null,
+			"song" => [],
+			"playlist" => [],
+			"album" => [],
+			"podcast" => [],
+			"author" => [],
+			"user" => []
+		];
+		
+		// get songs
+		foreach($payload["data"]["searchV2"]["tracksV2"]["items"] as $result){
+			
+			if(isset($result["item"])){
+				
+				$result = $result["item"];
+			}
+			
+			if(isset($result["data"])){
+				
+				$result = $result["data"];
+			}
+			
+			[$artist, $artist_link] = $this->get_artists($result["artists"]);
+			
+			$out["song"][] = [
+				"title" => $result["name"],
+				"description" => null,
+				"url" => "https://open.spotify.com/track/" . $result["id"],
+				"views" => null,
+				"author" => [
+					"name" => $artist,
+					"url" => $artist_link,
+					"avatar" => null
+				],
+				"thumb" => $this->get_thumb($result["albumOfTrack"]["coverArt"]),
+				"date" => null,
+				"duration" => $result["duration"]["totalMilliseconds"] / 1000,
+				"stream" => [
+					"endpoint" => "spotify",
+					"url" => "track." . $result["id"]
+				]
+			];
+		}
+		
+		// get playlists
+		foreach($payload["data"]["searchV2"]["playlists"]["items"] as $playlist){
+			
+			if(isset($playlist["data"])){
+				
+				$playlist = $playlist["data"];
+			}
+			
+			$avatar = $this->get_thumb($playlist["ownerV2"]["data"]["avatar"]);
+			
+			$out["playlist"][] = [
+				"title" => $playlist["name"],
+				"description" => null,
+				"author" => [
+					"name" => $playlist["ownerV2"]["data"]["name"],
+					"url" =>
+						"https://open.spotify.com/user/" .
+						explode(
+							":",
+							$playlist["ownerV2"]["data"]["uri"],
+							3
+						)[2],
+					"avatar" => $avatar["url"]
+				],
+				"thumb" => $this->get_thumb($playlist["images"]["items"][0]),
+				"date" => null,
+				"duration" => null,
+				"url" =>
+					"https://open.spotify.com/playlist/" .
+					explode(
+						":",
+						$playlist["uri"],
+						3
+					)[2]
+			];
+		}
+		
+		// get albums
+		foreach($payload["data"]["searchV2"]["albums"]["items"] as $album){
+			
+			if(isset($album["data"])){
+				
+				$album = $album["data"];
+			}
+			
+			[$artist, $artist_link] = $this->get_artists($album["artists"]);
+			
+			$out["album"][] = [
+				"title" => $album["name"],
+				"description" => null,
+				"author" => [
+					"name" => $artist,
+					"url" => $artist_link,
+					"avatar" => null
+				],
+				"thumb" => $this->get_thumb($album["coverArt"]),
+				"date" => mktime(0, 0, 0, 0, 32, $album["date"]["year"]),
+				"duration" => null,
+				"url" =>
+					"https://open.spotify.com/album/" .
+					explode(
+						":",
+						$album["uri"],
+						3
+					)[2]
+			];
+		}
+		
+		// get podcasts
+		foreach($payload["data"]["searchV2"]["podcasts"]["items"] as $podcast){
+			
+			if(isset($podcast["data"])){
+				
+				$podcast = $podcast["data"];
+			}
+			
+			$description = [];
+			foreach($podcast["topics"]["items"] as $subject){
+				
+				$description[] = $subject["title"];
+			}
+			
+			$description = implode(", ", $description);
+			
+			if($description == ""){
+				
+				$description = null;
+			}
+			
+			$out["podcast"][] = [
+				"title" => $podcast["name"],
+				"description" => $description,
+				"author" => [
+					"name" => $podcast["publisher"]["name"],
+					"url" => null,
+					"avatar" => null
+				],
+				"thumb" => $this->get_thumb($podcast["coverArt"]),
+				"date" => null,
+				"duration" => null,
+				"url" =>
+					"https://open.spotify.com/show/" .
+					explode(
+						":",
+						$podcast["uri"],
+						3
+					)[2],
+				"stream" => [
+					"endpoint" => null,
+					"url" => null
+				]
+			];
+		}
+		
+		// get audio books (put in podcasts)
+		foreach($payload["data"]["searchV2"]["audiobooks"]["items"] as $podcast){
+			
+			if(isset($podcast["data"])){
+				
+				$podcast = $podcast["data"];
+			}
+			
+			$description = [];
+			foreach($podcast["topics"]["items"] as $subject){
+				
+				$description[] = $subject["title"];
+			}
+			
+			$description = implode(", ", $description);
+			
+			if($description == ""){
+				
+				$description = null;
+			}
+			
+			$authors = [];
+			foreach($podcast["authors"] as $author){
+				
+				$authors[] = $author["name"];
+			}
+			
+			$authors = implode(", ", $authors);
+			
+			if($authors == ""){
+				
+				$authors = null;
+			}
+			
+			$uri =
+				explode(
+					":",
+					$podcast["uri"],
+					3
+				)[2];
+			
+			$out["podcast"][] = [
+				"title" => $podcast["name"],
+				"description" => $description,
+				"author" => [
+					"name" => $authors,
+					"url" => null,
+					"avatar" => null
+				],
+				"thumb" => $this->get_thumb($podcast["coverArt"]),
+				"date" => strtotime($podcast["publishDate"]["isoString"]),
+				"duration" => null,
+				"url" => "https://open.spotify.com/show/" . $uri,
+				"stream" => [
+					"endpoint" => "spotify",
+					"url" => "episode." . $uri
+				]
+			];
+		}
+		
+		// get episodes (and place them in podcasts)
+		foreach($payload["data"]["searchV2"]["episodes"]["items"] as $podcast){
+			
+			if(isset($podcast["data"])){
+				
+				$podcast = $podcast["data"];
+			}
+			
+			$out["podcast"][] = [
+				"title" => $podcast["name"],
+				"description" => $this->limitstrlen($podcast["description"]),
+				"author" => [
+					"name" =>
+						isset(
+							$podcast["podcastV2"]["data"]["publisher"]["name"]
+						) ?
+						$podcast["podcastV2"]["data"]["publisher"]["name"]
+						: null,
+					"url" => null,
+					"avatar" => null
+				],
+				"thumb" => $this->get_thumb($podcast["coverArt"]),
+				"date" => strtotime($podcast["releaseDate"]["isoString"]),
+				"duration" => $podcast["duration"]["totalMilliseconds"] / 1000,
+				"url" =>
+					"https://open.spotify.com/show/" .
+					explode(
+						":",
+						$podcast["uri"],
+						3
+					)[2],
+				"stream" => [
+					"endpoint" => null,
+					"url" => null
+				]
+			];
+		}
+		
+		// get authors
+		foreach($payload["data"]["searchV2"]["artists"]["items"] as $user){
+			
+			if(isset($user["data"])){
+				
+				$user = $user["data"];
+			}
+			
+			$avatar = $this->get_thumb($user["visuals"]["avatarImage"]);
+			
+			$out["author"][] = [
+				"title" =>
+					(
+						$user["profile"]["verified"] === true ?
+						"✓ " : ""
+					) .
+					$user["profile"]["name"],
+				"followers" => null,
+				"description" => null,
+				"thumb" => $avatar,
+				"url" =>
+					"https://open.spotify.com/artist/" .
+					explode(
+						":",
+						$user["uri"],
+						3
+					)[2]
+			];
+		}
+		
+		// get users
+		foreach($payload["data"]["searchV2"]["users"]["items"] as $user){
+			
+			if(isset($user["data"])){
+				
+				$user = $user["data"];
+			}
+			
+			$avatar = $this->get_thumb($user["avatar"]);
+			
+			$out["user"][] = [
+				"title" => $user["displayName"] . " (@{$user["id"]})",
+				"followers" => null,
+				"description" => null,
+				"thumb" => $avatar,
+				"url" => "https://open.spotify.com/user/" . $user["id"]
+			];
+		}
+		
+		return $out;
+	}
+	
+	private function get_artists($artists){
+		
+		$artist_out = [];
+		
+		foreach($artists["items"] as $artist){
+			
+			$artist_out[] = $artist["profile"]["name"];
+		}
+		
+		$artist_out =
+			implode(", ", $artist_out);
+		
+		if($artist_out == ""){
+			
+			return [null, null];
+		}
+		
+		$artist_link =
+			$artist === null ?
+			null :
+			"https://open.spotify.com/artist/" .
+			explode(
+				":",
+				$artists["items"][0]["uri"]
+			)[2];
+		
+		return [$artist_out, $artist_link];
+	}
+	
+	private function get_thumb($cover){
+		
+		$thumb_out = null;
+		
+		if($cover !== null){
+			foreach($cover["sources"] as $thumb){
+				
+				if(
+					$thumb_out === null ||
+					(int)$thumb["width"] > $thumb_out["width"]
+				){
+					
+					$thumb_out = $thumb;
+				}
+			}
+		}
+		
+		if($thumb_out === null){
+			
+			return [
+				"url" => null,
+				"ratio" => null
+			];
+		}else{
+			
+			return [
+				"url" => $thumb_out["url"],
+				"ratio" => "1:1"
+			];
+		}
+	}
+	
+	private function limitstrlen($text){
+		
+		return
+			explode(
+				"\n",
+				wordwrap(
+					str_replace(
+						["\n\r", "\r\n", "\n", "\r"],
+						" ",
+						$text
+					),
+					300,
+					"\n"
+				),
+				2
+			)[0];
+	}
+}

+ 1 - 1
settings.php

@@ -274,7 +274,6 @@ foreach($themes as $theme){
 /*
 /*
 	Set cookies
 	Set cookies
 */
 */
-
 if($_POST){
 if($_POST){
 
 
 	$loop = &$_POST;
 	$loop = &$_POST;
@@ -473,6 +472,7 @@ if(count($_GET) === 0){
 		$frontend->load(
 		$frontend->load(
 			"search.html",
 			"search.html",
 			[
 			[
+				"timetaken" => null,
 				"class" => "",
 				"class" => "",
 				"right-left" =>			
 				"right-left" =>			
 					'<div class="infobox"><h2>Preference link</h2>Following this link will re-apply all cookies configured here and will redirect you to the front page. Useful if your browser clears out cookies after a browsing session.<br><br>' .
 					'<div class="infobox"><h2>Preference link</h2>Following this link will re-apply all cookies configured here and will redirect you to the front page. Useful if your browser clears out cookies after a browsing session.<br><br>' .

+ 41 - 2
static/style.css

@@ -48,10 +48,35 @@ body{
 	font-size:16px;
 	font-size:16px;
 	box-sizing:border-box;
 	box-sizing:border-box;
 	font-family:sans-serif;
 	font-family:sans-serif;
-	padding:15px 7% 45px;
+	margin:15px 7% 45px;
 	word-wrap:anywhere;
 	word-wrap:anywhere;
 	line-height:22px;
 	line-height:22px;
 	max-width:2000px;
 	max-width:2000px;
+	position:relative;
+}
+
+.navigation{
+	position:absolute;
+	top:0;
+	right:0;
+	font-size:14px;
+	line-height:40px;
+}
+
+.navigation a{
+	color:var(--bdae93);
+	text-decoration:none;
+}
+
+.navigation a:hover{
+	text-decoration:underline;
+}
+
+.navigation a:not(:last-child)::after{
+	content:"|";
+	padding:0 7px;
+	display:inline-block;
+	color:var(--504945);
 }
 }
 
 
 h1,h2,h3,h4,h5,h6{
 h1,h2,h3,h4,h5,h6{
@@ -176,7 +201,6 @@ h3,h4,h5,h6{
 
 
 /* Filters */
 /* Filters */
 .filters{
 .filters{
-	padding-bottom:15px;
 	margin-bottom:7px;
 	margin-bottom:7px;
 }
 }
 
 
@@ -203,6 +227,12 @@ h3,h4,h5,h6{
 	height:24px;
 	height:24px;
 }
 }
 
 
+.timetaken{
+	font-size:14px;
+	font-weight:bold;
+	margin-bottom:10px;
+}
+
 
 
 /*
 /*
 	HOME
 	HOME
@@ -1288,6 +1318,15 @@ table tr a:last-child{
 }
 }
 
 
 @media only screen and (max-width: 1000px){
 @media only screen and (max-width: 1000px){
+	form{
+		padding-top:27px;
+	}
+	
+	.navigation{
+		left:0;
+		right:unset;
+		line-height:22px;
+	}
 	
 	
 	.nextpage.img{
 	.nextpage.img{
 		width:initial;
 		width:initial;

+ 5 - 0
template/header.html

@@ -12,6 +12,11 @@
 		<meta name="description" content="{%server_name%}: {%description%}">
 		<meta name="description" content="{%server_name%}: {%description%}">
 	</head>
 	</head>
 	<body>
 	<body>
+		<div class="navigation">
+			<a href="/">Home</a>
+			<a href="/settings">Settings</a>
+			<a href="https://git.lolcat.ca/lolcat/4get_news" target="_BLANK">News</a>
+		</div>
 		<form method="GET" autocomplete="off">
 		<form method="GET" autocomplete="off">
 			<div class="searchbox">
 			<div class="searchbox">
 				<input type="submit" value="Search" tabindex="-1">
 				<input type="submit" value="Search" tabindex="-1">

+ 1 - 1
template/home.html

@@ -28,7 +28,7 @@
 					<div class="autocomplete"></div>
 					<div class="autocomplete"></div>
 				</div>
 				</div>
 			</form>
 			</form>
-			<a href="settings">Settings</a> • <a href="instances">Instances</a> • <a href="api.txt">API</a> • <a href="about">About</a> • <a href="https://git.lolcat.ca/lolcat/4get">Source</a> • <a href="https://ko-fi.com/lolcat" rel="noreferrer" target="BLANK">Donate</a>
+			<a href="settings">Settings</a> • <a href="instances">Instances</a> • <a href="https://git.lolcat.ca/lolcat/4get_news">News</a> • <a href="api.txt">API</a> • <a href="about">About</a> • <a href="https://git.lolcat.ca/lolcat/4get">Source</a> • <a href="https://ko-fi.com/lolcat" rel="noreferrer" target="BLANK">Donate</a>
 			<div class="subtext">
 			<div class="subtext">
 				<a href="https://4get.ca">Clearnet</a> • <a href="http://4getwebfrq5zr4sxugk6htxvawqehxtdgjrbcn2oslllcol2vepa23yd.onion">Tor</a> • <a href="https://lolcat.ca">Report a problem</a><br>
 				<a href="https://4get.ca">Clearnet</a> • <a href="http://4getwebfrq5zr4sxugk6htxvawqehxtdgjrbcn2oslllcol2vepa23yd.onion">Tor</a> • <a href="https://lolcat.ca">Report a problem</a><br>
 				Running on <b>v{%version%}</b>!!
 				Running on <b>v{%version%}</b>!!

+ 1 - 0
template/images.html

@@ -1,3 +1,4 @@
+		<div class="timetaken">Took {%timetaken%}s</div>
 		<div id="images">
 		<div id="images">
 			{%images%}
 			{%images%}
 		</div>
 		</div>

+ 1 - 0
template/search.html

@@ -1,3 +1,4 @@
+		{%timetaken%}
 		<div id="overflow" class="web{%class%}">
 		<div id="overflow" class="web{%class%}">
 			<div class="right-wrapper">
 			<div class="right-wrapper">
 				<div class="right-right">
 				<div class="right-right">

+ 3 - 2
videos.php

@@ -15,10 +15,11 @@ $get = $frontend->parsegetfilters($_GET, $filters);
 /*
 /*
 	Captcha
 	Captcha
 */
 */
-include "lib/captcha_gen.php";
-new captcha($frontend, $get, $filters, "videos", true);
+include "lib/bot_protection.php";
+new bot_protection($frontend, $get, $filters, "videos", true);
 
 
 $payload = [
 $payload = [
+	"timetaken" => microtime(true),
 	"class" => "",
 	"class" => "",
 	"right-left" => "",
 	"right-left" => "",
 	"right-right" => "",
 	"right-right" => "",

+ 4 - 3
web.php

@@ -15,10 +15,11 @@ $get = $frontend->parsegetfilters($_GET, $filters);
 /*
 /*
 	Captcha
 	Captcha
 */
 */
-include "lib/captcha_gen.php";
-new captcha($frontend, $get, $filters, "web", true);
+include "lib/bot_protection.php";
+new bot_protection($frontend, $get, $filters, "web", true);
 
 
 $payload = [
 $payload = [
+	"timetaken" => microtime(true),
 	"class" => "",
 	"class" => "",
 	"right-left" => "",
 	"right-left" => "",
 	"right-right" => "",
 	"right-right" => "",
@@ -359,7 +360,7 @@ if(count($results["answer"]) !== 0){
 				
 				
 				case "audio":
 				case "audio":
 					$right["answer"] .=
 					$right["answer"] .=
-						'<audio src="/audio?s=' . urlencode($description["url"]) . '" controls><a href="/audio.php?s=' . urlencode($description["url"]) . '">Listen to the pronunciation audio</a></audio>';
+						'<audio src="/audio/linear?s=' . urlencode($description["url"]) . '" controls><a href="/audio/linear?s=' . urlencode($description["url"]) . '">Listen to the pronunciation audio</a></audio>';
 					break;
 					break;
 			}
 			}
 		}
 		}

Some files were not shown because too many files changed in this diff