123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726 |
- <?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];
- }
- }
|