boobs
This commit is contained in:
parent
edc917f5ee
commit
addc5a14a9
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
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -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 .
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue