This commit is contained in:
lolcat 2024-02-17 23:22:19 -05:00
parent edc917f5ee
commit addc5a14a9
27 changed files with 1521 additions and 215 deletions

31
api.txt
View File

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

20
audio/linear.php Normal file
View 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
View 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
View 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
View 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();
}
}

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
scraper/spotify.json Normal file

File diff suppressed because one or more lines are too long

726
scraper/spotify.php Normal file
View 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];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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