1
0

sc.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. <?php
  2. class sc{
  3. public function __construct(){
  4. include "lib/backend.php";
  5. $this->backend = new backend("sc");
  6. include "lib/fuckhtml.php";
  7. $this->fuckhtml = new fuckhtml();
  8. }
  9. public function getfilters($page){
  10. return [
  11. "type" => [
  12. "display" => "Type",
  13. "option" => [
  14. "any" => "Any type",
  15. "track" => "Tracks",
  16. "author" => "People",
  17. "album" => "Albums",
  18. "playlist" => "Playlists",
  19. "goplus" => "Go+ Tracks"
  20. ]
  21. ]
  22. ];
  23. }
  24. private function get($proxy, $url, $get = [], $web_req = false){
  25. $curlproc = curl_init();
  26. if($get !== []){
  27. $get = http_build_query($get);
  28. $url .= "?" . $get;
  29. }
  30. curl_setopt($curlproc, CURLOPT_URL, $url);
  31. curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
  32. // use http2
  33. curl_setopt($curlproc, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
  34. if($web_req === false){
  35. curl_setopt($curlproc, CURLOPT_HTTPHEADER,
  36. ["User-Agent: " . config::USER_AGENT,
  37. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
  38. "Accept-Language: en-US,en;q=0.5",
  39. "Accept-Encoding: gzip",
  40. "Referer: https://soundcloud.com/",
  41. "Origin: https://soundcloud.com",
  42. "DNT: 1",
  43. "Connection: keep-alive",
  44. "Sec-Fetch-Dest: empty",
  45. "Sec-Fetch-Mode: cors",
  46. "Sec-Fetch-Site: same-site",
  47. "Priority: u=1"]
  48. );
  49. }else{
  50. curl_setopt($curlproc, CURLOPT_HTTPHEADER,
  51. ["User-Agent: " . config::USER_AGENT,
  52. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
  53. "Accept-Language: en-US,en;q=0.5",
  54. "Accept-Encoding: gzip",
  55. "DNT: 1",
  56. "Connection: keep-alive",
  57. "Upgrade-Insecure-Requests: 1",
  58. "Sec-Fetch-Dest: document",
  59. "Sec-Fetch-Mode: navigate",
  60. "Sec-Fetch-Site: cross-site",
  61. "Priority: u=1",
  62. "TE: trailers"]
  63. );
  64. }
  65. curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
  66. curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
  67. curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
  68. curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
  69. curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
  70. $this->backend->assign_proxy($curlproc, $proxy);
  71. $data = curl_exec($curlproc);
  72. if(curl_errno($curlproc)){
  73. throw new Exception(curl_error($curlproc));
  74. }
  75. curl_close($curlproc);
  76. return $data;
  77. }
  78. public function music($get, $last_attempt = false){
  79. if($get["npt"]){
  80. [$params, $proxy] = $this->backend->get($get["npt"], "music");
  81. $params = json_decode($params, true);
  82. $url = $params["url"];
  83. unset($params["url"]);
  84. }else{
  85. // normal search:
  86. // https://api-v2.soundcloud.com/search?q=freddie%20dredd&variant_ids=&facet=model&user_id=351062-302234-707916-795081&client_id=iMxZgT5mfGstBj8GWJbYMvpzelS8ne0E&limit=20&offset=0&linked_partitioning=1&app_version=1693487844&app_locale=en
  87. // soundcloud go+ search:
  88. // https://api-v2.soundcloud.com/search/tracks?q=freddie%20dredd&variant_ids=&filter.content_tier=SUB_HIGH_TIER&facet=genre&user_id=630591-269800-703400-765403&client_id=iMxZgT5mfGstBj8GWJbYMvpzelS8ne0E&limit=20&offset=0&linked_partitioning=1&app_version=1693487844&app_locale=en
  89. // tracks search:
  90. // https://api-v2.soundcloud.com/search/tracks?q=freddie%20dredd&variant_ids=&facet=genre&user_id=630591-269800-703400-765403&client_id=iMxZgT5mfGstBj8GWJbYMvpzelS8ne0E&limit=20&offset=0&linked_partitioning=1&app_version=1693487844&app_locale=en
  91. // users search:
  92. // https://api-v2.soundcloud.com/search/users?q=freddie%20dredd&variant_ids=&facet=place&user_id=630591-269800-703400-765403&client_id=iMxZgT5mfGstBj8GWJbYMvpzelS8ne0E&limit=20&offset=0&linked_partitioning=1&app_version=1693487844&app_locale=en
  93. // albums search:
  94. // https://api-v2.soundcloud.com/search/albums?q=freddie%20dredd&variant_ids=&facet=genre&user_id=630591-269800-703400-765403&client_id=iMxZgT5mfGstBj8GWJbYMvpzelS8ne0E&limit=20&offset=0&linked_partitioning=1&app_version=1693487844&app_locale=en
  95. // playlists search:
  96. // https://api-v2.soundcloud.com/search/playlists_without_albums?q=freddie%20dredd&variant_ids=&facet=genre&user_id=630591-269800-703400-765403&client_id=iMxZgT5mfGstBj8GWJbYMvpzelS8ne0E&limit=20&offset=0&linked_partitioning=1&app_version=1693487844&app_locale=en
  97. $search = $get["s"];
  98. if(strlen($search) === 0){
  99. throw new Exception("Search term is empty!");
  100. }
  101. $type = $get["type"];
  102. $proxy = $this->backend->get_ip();
  103. // token is not tied to an IP address
  104. $token = $this->get_token($proxy);
  105. switch($type){
  106. case "any":
  107. $url = "https://api-v2.soundcloud.com/search";
  108. $params = [
  109. "q" => $search,
  110. "variant_ids" => "",
  111. "facet" => "model",
  112. "client_id" => $token,
  113. "limit" => 20,
  114. "offset" => 0,
  115. "linked_partitioning" => 1,
  116. "app_version" => 1769170554,
  117. "app_locale" => "en"
  118. ];
  119. break;
  120. case "track":
  121. $url = "https://api-v2.soundcloud.com/search/tracks";
  122. $params = [
  123. "q" => $search,
  124. "variant_ids" => "",
  125. "facet_genre" => "",
  126. "client_id" => $token,
  127. "limit" => 20,
  128. "offset" => 0,
  129. "linked_partitioning" => 1,
  130. "app_version" => 1769170554,
  131. "app_locale" => "en"
  132. ];
  133. break;
  134. case "author":
  135. $url = "https://api-v2.soundcloud.com/search/users";
  136. $params = [
  137. "q" => $search,
  138. "variant_ids" => "",
  139. "facet" => "place",
  140. "client_id" => $token,
  141. "limit" => 20,
  142. "offset" => 0,
  143. "linked_partitioning" => 1,
  144. "app_version" => 1769170554,
  145. "app_locale" => "en"
  146. ];
  147. break;
  148. case "album":
  149. $url = "https://api-v2.soundcloud.com/search/albums";
  150. $params = [
  151. "q" => $search,
  152. "variant_ids" => "",
  153. "facet" => "genre",
  154. "client_id" => $token,
  155. "limit" => 20,
  156. "offset" => 0,
  157. "linked_partitioning" => 1,
  158. "app_version" => 1769170554,
  159. "app_locale" => "en"
  160. ];
  161. break;
  162. case "playlist":
  163. $url = "https://api-v2.soundcloud.com/search/playlists_without_albums";
  164. $params = [
  165. "q" => $search,
  166. "variant_ids" => "",
  167. "facet" => "genre",
  168. "client_id" => $token,
  169. "limit" => 20,
  170. "offset" => 0,
  171. "linked_partitioning" => 1,
  172. "app_version" => 1769170554,
  173. "app_locale" => "en"
  174. ];
  175. break;
  176. case "goplus":
  177. $url = "https://api-v2.soundcloud.com/search/tracks";
  178. $params = [
  179. "q" => $search,
  180. "variant_ids" => "",
  181. "filter.content_tier" => "SUB_HIGH_TIER",
  182. "facet" => "genre",
  183. "client_id" => $token,
  184. "limit" => 20,
  185. "offset" => 0,
  186. "linked_partitioning" => 1,
  187. "app_version" => 1769170554,
  188. "app_locale" => "en"
  189. ];
  190. break;
  191. }
  192. }
  193. try{
  194. $json = $this->get($proxy, $url, $params);
  195. }catch(Exception $error){
  196. throw new Exception("Failed to fetch JSON");
  197. }
  198. /*
  199. $handle = fopen("scraper/soundcloud.json", "r");
  200. $json = fread($handle, filesize("scraper/soundcloud.json"));
  201. fclose($handle);
  202. */
  203. $this->fuckhtml->load($json);
  204. // detect for cloudfront error
  205. $title =
  206. $this->fuckhtml
  207. ->getElementsByTagName(
  208. "title"
  209. );
  210. if(
  211. count($title) !== 0 &&
  212. str_contains($title[0]["innerHTML"], "The request could not be satisfied")
  213. ){
  214. throw new Exception("Ratelimited by Cloudfront, add some working proxies");
  215. }
  216. $json = json_decode($json, true);
  217. if($json === null){
  218. if($last_attempt === true){
  219. throw new Exception("Fetched an invalid token");
  220. }
  221. // token might've expired, get a new one and re-try search
  222. $token = $this->get_token($proxy, true);
  223. return $this->music($get, true);
  224. }
  225. $out = [
  226. "status" => "ok",
  227. "npt" => null,
  228. "song" => [],
  229. "playlist" => [],
  230. "album" => [],
  231. "podcast" => [],
  232. "author" => [],
  233. "user" => []
  234. ];
  235. /*
  236. Get next page
  237. */
  238. if(isset($json["next_href"])){
  239. $params["query_urn"] = $json["query_urn"];
  240. $params["offset"] = $params["offset"] + 20;
  241. $params["url"] = $url; // we will remove this later
  242. $out["npt"] =
  243. $this->backend->store(
  244. json_encode($params),
  245. "music",
  246. $proxy
  247. );
  248. }
  249. /*
  250. Scrape items
  251. */
  252. foreach($json["collection"] as $item){
  253. switch($item["kind"]){
  254. case "user":
  255. // parse author
  256. $out["author"][] = [
  257. "title" => $item["username"],
  258. "followers" => $item["followers_count"],
  259. "description" => trim($item["track_count"] . " songs. " . $this->limitstrlen($item["description"])),
  260. "thumb" => [
  261. "url" => $item["avatar_url"],
  262. "ratio" => "1:1"
  263. ],
  264. "url" => $item["permalink_url"]
  265. ];
  266. break;
  267. case "playlist":
  268. // parse playlist
  269. $description = [];
  270. $count = 0;
  271. foreach($item["tracks"] as $song){
  272. $count++;
  273. if(!isset($song["title"])){
  274. continue;
  275. }
  276. $description[] = $song["title"];
  277. }
  278. if(count($description) !== 0){
  279. $description = trim($count . " songs. " . implode(", ", $description));
  280. }else{
  281. $description = "";
  282. }
  283. if(
  284. isset($item["artwork_url"]) &&
  285. !empty($item["artwork_url"])
  286. ){
  287. $thumb = [
  288. "ratio" => "1:1",
  289. "url" => $item["artwork_url"]
  290. ];
  291. }elseif(
  292. isset($item["tracks"][0]["artwork_url"]) &&
  293. !empty($item["tracks"][0]["artwork_url"])
  294. ){
  295. $thumb = [
  296. "ratio" => "1:1",
  297. "url" => $item["tracks"][0]["artwork_url"]
  298. ];
  299. }else{
  300. $thumb = [
  301. "ratio" => null,
  302. "url" => null
  303. ];
  304. }
  305. $out["playlist"][] = [
  306. "title" => $item["title"],
  307. "description" => $this->limitstrlen($description),
  308. "author" => [
  309. "name" => $item["user"]["username"],
  310. "url" => $item["user"]["permalink_url"],
  311. "avatar" => $item["user"]["avatar_url"]
  312. ],
  313. "thumb" => $thumb,
  314. "date" => strtotime($item["created_at"]),
  315. "duration" => $item["duration"] / 1000,
  316. "url" => $item["permalink_url"]
  317. ];
  318. break;
  319. case "track":
  320. $stream = [
  321. "endpoint" => null,
  322. "url" => null
  323. ];
  324. /*if(stripos($item["monetization_model"], "TIER") === false){
  325. $stream = [
  326. "endpoint" => "sc",
  327. "url" =>
  328. $item["media"]["transcodings"][0]["url"] .
  329. "?client_id=" . $token .
  330. "&track_authorization=" .
  331. $item["track_authorization"]
  332. ];
  333. }else{
  334. $stream = [
  335. "endpoint" => null,
  336. "url" => null
  337. ];
  338. }*/
  339. // parse track
  340. $out["song"][] = [
  341. "title" => $item["title"],
  342. "description" => $item["description"] == "" ? null : $this->limitstrlen($item["description"]),
  343. "url" => $item["permalink_url"],
  344. "views" => $item["playback_count"],
  345. "author" => [
  346. "name" => $item["user"]["username"],
  347. "url" => $item["user"]["permalink_url"],
  348. "avatar" => $item["user"]["avatar_url"]
  349. ],
  350. "thumb" => [
  351. "ratio" => "1:1",
  352. "url" => $item["artwork_url"]
  353. ],
  354. "date" => strtotime($item["created_at"]),
  355. "duration" => (int)$item["full_duration"] / 1000,
  356. "stream" => $stream
  357. ];
  358. break;
  359. }
  360. }
  361. return $out;
  362. }
  363. public function get_token($proxy, $force_refresh = false){
  364. $token = apcu_fetch("sc_token");
  365. if(
  366. $token !== false &&
  367. $force_refresh === false
  368. ){
  369. return $token;
  370. }
  371. // get token from main page
  372. try{
  373. $html =
  374. $this->get(
  375. $proxy,
  376. "https://soundcloud.com",
  377. [],
  378. true
  379. );
  380. }catch(Exception $error){
  381. throw new Exception("Failed to fetch front page");
  382. }
  383. $this->fuckhtml->load($html);
  384. $json =
  385. preg_split(
  386. '/window.__sc_hydration ?= ?/',
  387. $html
  388. );
  389. if(!isset($json[1])){
  390. throw new Exception("Failed to find JSON containing token");
  391. }
  392. $json =
  393. json_decode(
  394. $this->fuckhtml
  395. ->extract_json(
  396. $json[1]
  397. ),
  398. true
  399. );
  400. if($json === null){
  401. throw new Exception("Failed to decode JSON containing token");
  402. }
  403. foreach($json as $item){
  404. if(
  405. isset($item["hydratable"]) &&
  406. isset($item["data"]["id"])
  407. ){
  408. apcu_store("sc_token", $item["data"]["id"]);
  409. return $item["data"]["id"];
  410. }
  411. }
  412. throw new Exception("Failed to find a valid token");
  413. }
  414. private function limitstrlen($text){
  415. return
  416. explode(
  417. "\n",
  418. wordwrap(
  419. str_replace(
  420. ["\n\r", "\r\n", "\n", "\r"],
  421. " ",
  422. $text
  423. ),
  424. 300,
  425. "\n"
  426. ),
  427. 2
  428. )[0];
  429. }
  430. }