lolcat hace 2 años
padre
commit
addc5a14a9
Se han modificado 27 ficheros con 1519 adiciones y 214 borrados
  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
     looks like this ::
     
-        endpoint: audio_sc
+        endpoint: sc
         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
     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 ::
         
@@ -313,14 +316,14 @@
     is set.
 
 
-+ /audio
++ /audio/linear
     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.
 
 
-+ /audio_sc
++ /audio/sc
     Get a proxied audio file for SoundCloud. Does not support downloads
     trough WGET or CURL, since it returns 30kb~160kb "206 Partial
     Content" parts, due to technical limitations that comes with
@@ -334,6 +337,14 @@
     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
     If you have any questions or need clarifications, please send an
     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
 
 if(
-	!isset($_GET["k"]) ||
+	isset($_GET["v"]) === false ||
+	is_array($_GET["v"]) === true ||
 	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");
-	echo "Fuck you";
+	echo "Fuck my feathered cloaca";
 	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
 	die();
 }
 
+header("Content-Type: image/jpeg");
 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(
-	$_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
-
 if(random_int(0,1) === 0){
 	
 	$theme = [
@@ -57,7 +114,7 @@ if(random_int(0,1) === 0){
 }
 
 $im = new Imagick();
-$im->newImage(400, 400, $theme["bg"]);
+$im->newImage(400, 427, $theme["bg"]);
 $im->setImageBackgroundColor($theme["bg"]);
 $im->setImageFormat("jpg");
 
@@ -76,12 +133,18 @@ for($y=0; $y<4; $y++){
 	
 	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
 		$tmp->setImageBackgroundColor("black");
 		$tmp->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
-
+		
+		// randomly mirror
+		if(random_int(0,1) === 1){
+			
+			$tmp->flopImage();
+		}
+		
 		// distort $tmp
 		$tmp->distortImage(
 			$distort[random_int(0,1)],
@@ -101,21 +164,15 @@ for($y=0; $y<4; $y++){
 			false
 		);
 		
+		$tmp->addNoiseImage($noise[random_int(0, 1)]);
+		
 		// 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++;
 	}
 }
 
-// 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
 $draw = new ImagickDraw();
 $draw->setFontSize(20);
@@ -123,7 +180,7 @@ $draw->setFillColor($theme["fg"]);
 //$draw->setTextAntialias(false);
 $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);
 
@@ -143,5 +200,4 @@ for($i=0; $i<strlen($text); $i++){
 
 $im->setFormat("jpeg");
 $im->setImageCompressionQuality(90);
-$im->setImageCompression(Imagick::COMPRESSION_JPEG2000);
 echo $im->getImageBlob();

+ 18 - 9
data/config.php

@@ -5,7 +5,7 @@ class config{
 	// any parameters.
 	
 	// 4get version. Please keep this updated
-	const VERSION = 6;
+	const VERSION = 7;
 	
 	// Will be shown pretty much everywhere.
 	const SERVER_NAME = "4get";
@@ -24,10 +24,10 @@ class config{
 	const API_ENABLED = true;
 	
 	// 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...
 	// 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)
 	const BOT_PROTECTION = 0;
 	
@@ -62,20 +62,27 @@ class config{
 		"https://4get.zzls.xyz",
 		"https://4getus.zzls.xyz",
 		"https://4get.silly.computer",
-		"https://4g.opnxng.com",
 		"https://4get.konakona.moe",
 		"https://4get.lvkaszus.pl",
 		"https://4g.ggtyler.dev",
 		"https://4get.perennialte.ch",
-		"https://4get.sihj.net",
+		"https://4get.sijh.net",
 		"https://4get.hbubli.cc",
 		"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
 	// 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
 	// false = Use server's raw IP
@@ -94,6 +101,8 @@ class config{
 	const PROXY_YT = false; // youtube
 	const PROXY_YEP = false;
 	const PROXY_PINTEREST = false;
+	const PROXY_SEZNAM = false;
+	const PROXY_NAVER = false;
 	const PROXY_FTM = false; // findthatmeme
 	const PROXY_IMGUR = false;
 	const PROXY_YANDEX_W = false; // yandex web
@@ -107,8 +116,8 @@ class config{
 	// SOUNDCLOUD
 	// Get these parameters by making a search on soundcloud with network
 	// 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
 	// 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
 */
-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 = [
+	"timetaken" => microtime(true),
 	"images" => "",
 	"nextpage" => ""
 ];

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

@@ -1,6 +1,6 @@
 <?php
 
-class captcha{
+class bot_protection{
 	
 	public function __construct($frontend, $get, $filters, $page, $output){
 		
@@ -26,7 +26,7 @@ class captcha{
 			if(
 				// check if key is not malformed
 				preg_match(
-					'/^c[0-9]+\.[A-Za-z0-9]{20}$/',
+					'/^k[0-9]+\.[A-Za-z0-9_]{20}$/',
 					$_COOKIE["pass"]
 				) &&
 				// does key exist
@@ -39,7 +39,7 @@ class captcha{
 				// we start counting from 1
 				// when it has been incremented to 102, it has reached
 				// 100 reqs
-				if($inc >= 102){
+				if($inc >= config::MAX_SEARCHES + 2){
 					
 					// reached limit, delete and give captcha
 					apcu_delete($_COOKIE["pass"]);
@@ -62,7 +62,7 @@ class captcha{
 		
 		if($output === false){
 			
-			http_response_code(429); // too many reqs
+			http_response_code(401); // forbidden
 			echo json_encode([
 				"status" => "The \"pass\" token in your cookies is missing or has expired!!"
 			]);
@@ -104,10 +104,13 @@ class captcha{
 				!isset($regex[0][1])
 			){
 				
-				// check if its k
+				// check if its the v key
 				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]);
@@ -129,27 +132,21 @@ class captcha{
 			
 			$answers[] = $regex;
 		}
-
+		
 		if(
 			!$invalid &&
-			$key !== false
+			$key !== false // has captcha been gen'd?
 		){
-			$check = $key[1];
+			$check = count($key);
 			
 			// 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--;
 				}else{
 					
-					// got a wrong answer
 					$check = -1;
 					break;
 				}
@@ -160,21 +157,8 @@ class captcha{
 				// we passed the captcha
 				// set 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);
 				
@@ -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 = [
+			"timetaken" => microtime(true),
 			"class" => "",
 			"right-left" => "",
 			"right-right" => "",
 			"left" =>
 				'<div class="infobox">' .
 					'<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 .
 					'<form method="POST" enctype="text/plain" autocomplete="off">' .
 						'<div class="captcha-wrapper">' .
 							'<div class="captcha">' .
-								'<img src="captcha?k=' . $key . '" alt="Captcha image">' .
+								'<img src="captcha.php?v=' . $key . '" alt="Captcha image">' .
 								'<div class="captcha-controls">' .
 									'<input type="checkbox" name="c[0]" id="c0">' .
 									'<label for="c0"></label>' .
@@ -317,13 +240,12 @@ class captcha{
 								'</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">' .
 					'</form>' .
 				'</div>'
 		];
 		
-		http_response_code(429); // too many reqs
 		$frontend->loadheader(
 			$get,
 			$filters,
@@ -333,4 +255,27 @@ class captcha{
 		echo $frontend->load("search.html", $payload);
 		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"] = '';
 		}
 		
+		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");
 		$data = fread($handle, filesize("template/{$template}"));
 		fclose($handle);
@@ -68,7 +76,7 @@ class frontend{
 		
 		echo
 			$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;',
 				"index" => "no",
 				"search" => htmlspecialchars($get["s"]),
@@ -88,7 +96,7 @@ class frontend{
 			
 			$this->drawerror(
 				"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();
 		}
@@ -98,6 +106,7 @@ class frontend{
 		
 		echo
 			$this->load("search.html", [
+				"timetaken" => null,
 				"class" => "",
 				"right-left" => "",
 				"right-right" => "",

+ 16 - 9
lib/fuckhtml.php

@@ -466,19 +466,26 @@ class fuckhtml{
 		
 		return
 			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){
 					
-					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

+ 28 - 19
music.php

@@ -15,10 +15,11 @@ $get = $frontend->parsegetfilters($_GET, $filters);
 /*
 	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 = [
+	"timetaken" => microtime(true),
 	"class" => "",
 	"right-left" => "",
 	"right-right" => "",
@@ -36,7 +37,10 @@ try{
 $categories = [
 	"song" => "",
 	"author" => "",
-	"playlist" => ""
+	"playlist" => "",
+	"album" => "",
+	"podcast" => "",
+	"user" => ""
 ];
 
 /*
@@ -48,14 +52,26 @@ if(count($results["song"]) !== 0){
 	
 	$main = "song";
 	
-}elseif(count($results["author"]) !== 0){
+}elseif(count($results["album"]) !== 0){
 	
-	$main = "author";
+	$main = "album";
 	
 }elseif(count($results["playlist"]) !== 0){
 	
 	$main = "playlist";
 	
+}elseif(count($results["podcast"]) !== 0){
+	
+	$main = "podcast";
+
+}elseif(count($results["author"]) !== 0){
+	
+	$main = "author";
+		
+}elseif(count($results["user"]) !== 0){
+	
+	$main = "user";
+	
 }else{
 	
 	// No results found!
@@ -133,12 +149,15 @@ foreach($categories as $name => $data){
 		$customhtml = null;
 		
 		if(
-			$name == "song" &&
+			(
+				$name == "song" ||
+				$name == "podcast"
+			) &&
 			$item["stream"]["endpoint"] !== null
 		){
 			
 			$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);
@@ -177,18 +196,8 @@ foreach($categories as $name => $value){
 			'<div class="answer-title">' .
 				'<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] .=
 			'</div>' .

+ 3 - 2
news.php

@@ -15,10 +15,11 @@ $get = $frontend->parsegetfilters($_GET, $filters);
 /*
 	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 = [
+	"timetaken" => microtime(true),
 	"class" => "",
 	"right-left" => "",
 	"right-right" => "",

+ 1 - 1
opensearch.php

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

+ 7 - 4
scraper/sc.php

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

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 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
 */
-
 if($_POST){
 
 	$loop = &$_POST;
@@ -473,6 +472,7 @@ if(count($_GET) === 0){
 		$frontend->load(
 			"search.html",
 			[
+				"timetaken" => null,
 				"class" => "",
 				"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>' .

+ 41 - 2
static/style.css

@@ -48,10 +48,35 @@ body{
 	font-size:16px;
 	box-sizing:border-box;
 	font-family:sans-serif;
-	padding:15px 7% 45px;
+	margin:15px 7% 45px;
 	word-wrap:anywhere;
 	line-height:22px;
 	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{
@@ -176,7 +201,6 @@ h3,h4,h5,h6{
 
 /* Filters */
 .filters{
-	padding-bottom:15px;
 	margin-bottom:7px;
 }
 
@@ -203,6 +227,12 @@ h3,h4,h5,h6{
 	height:24px;
 }
 
+.timetaken{
+	font-size:14px;
+	font-weight:bold;
+	margin-bottom:10px;
+}
+
 
 /*
 	HOME
@@ -1288,6 +1318,15 @@ table tr a:last-child{
 }
 
 @media only screen and (max-width: 1000px){
+	form{
+		padding-top:27px;
+	}
+	
+	.navigation{
+		left:0;
+		right:unset;
+		line-height:22px;
+	}
 	
 	.nextpage.img{
 		width:initial;

+ 5 - 0
template/header.html

@@ -12,6 +12,11 @@
 		<meta name="description" content="{%server_name%}: {%description%}">
 	</head>
 	<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">
 			<div class="searchbox">
 				<input type="submit" value="Search" tabindex="-1">

+ 1 - 1
template/home.html

@@ -28,7 +28,7 @@
 					<div class="autocomplete"></div>
 				</div>
 			</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">
 				<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>!!

+ 1 - 0
template/images.html

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

+ 1 - 0
template/search.html

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

+ 3 - 2
videos.php

@@ -15,10 +15,11 @@ $get = $frontend->parsegetfilters($_GET, $filters);
 /*
 	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 = [
+	"timetaken" => microtime(true),
 	"class" => "",
 	"right-left" => "",
 	"right-right" => "",

+ 4 - 3
web.php

@@ -15,10 +15,11 @@ $get = $frontend->parsegetfilters($_GET, $filters);
 /*
 	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 = [
+	"timetaken" => microtime(true),
 	"class" => "",
 	"right-left" => "",
 	"right-right" => "",
@@ -359,7 +360,7 @@ if(count($results["answer"]) !== 0){
 				
 				case "audio":
 					$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;
 			}
 		}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio