boobs
This commit is contained in:
		
							
								
								
									
										31
									
								
								api.txt
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								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
									
								
								audio/linear.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								audio/linear.php
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
								
								audio/sc.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								audio/sc.php
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
								
								audio/seekable.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								audio/seekable.php
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
								
								audio/spotify.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								audio/spotify.php
									
									
									
									
									
										Normal file
									
								
							| @@ -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(); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										128
									
								
								captcha.php
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								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"; | ||||
|  | ||||
| $grid = apcu_fetch($_GET["k"]); | ||||
|  | ||||
| if( | ||||
| 	$grid === false || | ||||
| 	$grid[3] === true // has already been generated | ||||
| ){ | ||||
| if(config::BOT_PROTECTION !== 1){ | ||||
| 	 | ||||
| 	header("Content-Type: text/plain"); | ||||
| 	echo "The IQ test is disabled"; | ||||
| 	die(); | ||||
| } | ||||
|  | ||||
| $grid = apcu_fetch($_GET["v"]); | ||||
|  | ||||
| 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(); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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" => "" | ||||
| ]; | ||||
|   | ||||
| @@ -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++){ | ||||
| 			for($i=0; $i<count($answers); $i++){ | ||||
| 				 | ||||
| 				if(!in_array($i, $answers)){ | ||||
| 					 | ||||
| 					continue; | ||||
| 				} | ||||
| 				 | ||||
| 				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
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								lib/classic.png
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 358 B | 
| @@ -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 "' . htmlspecialchars($get["s"]) . '"', | ||||
| 				"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" => "", | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										47
									
								
								music.php
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								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>' . | ||||
|   | ||||
							
								
								
									
										5
									
								
								news.php
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								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" => "", | ||||
|   | ||||
| @@ -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"] | ||||
| 	); | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
| 			} | ||||
|   | ||||
| @@ -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 . | ||||
|   | ||||
							
								
								
									
										1
									
								
								scraper/spotify.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								scraper/spotify.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										726
									
								
								scraper/spotify.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										726
									
								
								scraper/spotify.php
									
									
									
									
									
										Normal file
									
								
							| @@ -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]; | ||||
| 	} | ||||
| } | ||||
| @@ -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>' . | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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,3 +1,4 @@ | ||||
| 		<div class="timetaken">Took {%timetaken%}s</div> | ||||
| 		<div id="images"> | ||||
| 			{%images%} | ||||
| 		</div> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| 		{%timetaken%} | ||||
| 		<div id="overflow" class="web{%class%}"> | ||||
| 			<div class="right-wrapper"> | ||||
| 				<div class="right-right"> | ||||
|   | ||||
| @@ -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" => "", | ||||
|   | ||||
							
								
								
									
										7
									
								
								web.php
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								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; | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user