coccoc.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. <?php
  2. class coccoc{
  3. public function __construct(){
  4. include "lib/backend.php";
  5. $this->backend = new backend("coccoc");
  6. include "lib/fuckhtml.php";
  7. $this->fuckhtml = new fuckhtml();
  8. }
  9. private function get($proxy, $url, $get = []){
  10. $curlproc = curl_init();
  11. if($get !== []){
  12. $get = http_build_query($get);
  13. $url .= "?" . $get;
  14. }
  15. curl_setopt($curlproc, CURLOPT_URL, $url);
  16. // http2 bypass
  17. curl_setopt($curlproc, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
  18. curl_setopt($curlproc, CURLOPT_HTTPHEADER, [
  19. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
  20. "Accept-Language: en-US,en;q=0.5",
  21. "Accept-Encoding: gzip, deflate, br, zstd",
  22. "DNT: 1",
  23. "Sec-GPC: 1",
  24. "Connection: keep-alive",
  25. //"Cookie: _contentAB_15040_vi=V-06_01; split_test_search=new_search; uid=L_bauXyZBY1B; vid=uCVQJQSTgb9QGT3o; ls=1753742684; serp_version=29223843/7621a70; savedS=direct",
  26. "Upgrade-Insecure-Requests: 1",
  27. "Sec-Fetch-Dest: document",
  28. "Sec-Fetch-Mode: navigate",
  29. "Sec-Fetch-Site: cross-site",
  30. "Priority: u=0, i"
  31. ]);
  32. $this->backend->assign_proxy($curlproc, $proxy);
  33. curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
  34. curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
  35. curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
  36. curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
  37. curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
  38. curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
  39. $data = curl_exec($curlproc);
  40. if(curl_errno($curlproc)){
  41. throw new Exception(curl_error($curlproc));
  42. }
  43. curl_close($curlproc);
  44. return $data;
  45. }
  46. public function getfilters($pagetype){
  47. return [
  48. "nsfw" => [
  49. "display" => "NSFW",
  50. "option" => [
  51. "yes" => "Yes", // nsfw by default????
  52. "no" => "No" // &safe=1
  53. ]
  54. ],
  55. "time" => [
  56. "display" => "Time posted",
  57. "option" => [
  58. "any" => "Any time",
  59. "1w" => "1 week ago",
  60. "2w" => "2 weeks ago",
  61. "1m" => "1 month ago",
  62. "3m" => "3 months ago",
  63. "6m" => "6 months ago",
  64. "1Y" => "1 year ago"
  65. ]
  66. ],
  67. "filter" => [
  68. "display" => "Remove duplicates",
  69. "option" => [
  70. "no" => "No",
  71. "yes" => "Yes" // &filter=0
  72. ]
  73. ]
  74. ];
  75. }
  76. public function web($get){
  77. if($get["npt"]){
  78. [$query, $proxy] =
  79. $this->backend->get(
  80. $get["npt"],
  81. "web"
  82. );
  83. $query = json_decode($query, true);
  84. }else{
  85. $proxy = $this->backend->get_ip();
  86. $query = [
  87. "query" => $get["s"]
  88. ];
  89. // add filters
  90. if($get["nsfw"] == "no"){
  91. $query["safe"] = 1;
  92. }
  93. if($get["time"] != "any"){
  94. $query["tbs"] = $get["time"];
  95. }
  96. if($get["filter"] == "yes"){
  97. $query["filter"] = 0;
  98. }
  99. }
  100. try{
  101. $html =
  102. $this->get(
  103. $proxy,
  104. "https://coccoc.com/search",
  105. $query
  106. );
  107. }catch(Exception $error){
  108. throw new Exception("Failed to get search page");
  109. }
  110. //$html = file_get_contents("scraper/coccoc.html");
  111. $html = explode("window.composerResponse", $html, 2);
  112. if(count($html) !== 2){
  113. throw new Exception("Failed to grep window.composerResponse");
  114. }
  115. $html =
  116. json_decode(
  117. $this->fuckhtml
  118. ->extract_json(
  119. ltrim($html[1], " =")
  120. ),
  121. true
  122. );
  123. if($html === null){
  124. throw new Exception("Failed to decode JSON");
  125. }
  126. if(!isset($html["search"]["search_results"])){
  127. throw new Exception("Coc Coc did not return a search_results object");
  128. }
  129. $out = [
  130. "status" => "ok",
  131. "spelling" => [
  132. "type" => "no_correction",
  133. "using" => null,
  134. "correction" => null
  135. ],
  136. "npt" => null,
  137. "answer" => [],
  138. "web" => [],
  139. "image" => [],
  140. "video" => [],
  141. "news" => [],
  142. "related" => []
  143. ];
  144. // word correction
  145. foreach($html["top"] as $element){
  146. if(isset($element["spellChecker"][0]["query"])){
  147. $out["spelling"] = [
  148. "type" => "not_many",
  149. "using" => $html["search"]["query"],
  150. "correction" => $element["spellChecker"][0]["query"]
  151. ];
  152. }
  153. }
  154. foreach($html["search"]["search_results"] as $result){
  155. if(isset($result["type"])){
  156. switch($result["type"]){
  157. //
  158. // Related searches
  159. //
  160. case "related_queries":
  161. $out["related"] = $result["queries"];
  162. continue 2;
  163. //
  164. // Videos
  165. //
  166. case "video_hits":
  167. foreach($result["results"] as $video){
  168. if(
  169. isset($video["image_url"]) &&
  170. !empty($video["image_url"])
  171. ){
  172. $thumb = [
  173. "ratio" => "16:9",
  174. "url" => $video["image_url"]
  175. ];
  176. }else{
  177. $thumb = [
  178. "ratio" => null,
  179. "url" => null
  180. ];
  181. }
  182. $out["video"][] = [
  183. "title" =>
  184. $this->titledots(
  185. $this->fuckhtml
  186. ->getTextContent(
  187. $video["title"]
  188. )
  189. ),
  190. "description" => null,
  191. "author" => [
  192. "name" => $video["uploader"],
  193. "url" => null,
  194. "avatar" => null
  195. ],
  196. "date" => (int)$video["date"],
  197. "duration" => (int)$video["duration"],
  198. "views" => null,
  199. "thumb" => $thumb,
  200. "url" => $video["url"]
  201. ];
  202. }
  203. continue 2;
  204. }
  205. }
  206. if(
  207. !isset($result["title"]) ||
  208. !isset($result["url"])
  209. ){
  210. // should not happen
  211. continue;
  212. }
  213. if(isset($result["rich"]["data"]["image_url"])){
  214. $thumb = [
  215. "url" => $result["rich"]["data"]["image_url"],
  216. "ratio" => "16:9"
  217. ];
  218. }else{
  219. $thumb = [
  220. "url" => null,
  221. "ratio" => null
  222. ];
  223. }
  224. $sublinks = [];
  225. if(isset($result["rich"]["data"]["linked_docs"])){
  226. foreach($result["rich"]["data"]["linked_docs"] as $sub){
  227. $sublinks[] = [
  228. "title" =>
  229. $this->titledots(
  230. $this->fuckhtml
  231. ->getTextContent(
  232. $sub["title"]
  233. )
  234. ),
  235. "description" =>
  236. $this->titledots(
  237. $this->fuckhtml
  238. ->getTextContent(
  239. $sub["content"]
  240. )
  241. ),
  242. "date" => null,
  243. "url" => $sub["url"]
  244. ];
  245. }
  246. }
  247. // get date
  248. if(isset($result["date"])){
  249. $date = (int)$result["date"];
  250. }else{
  251. $date = null;
  252. }
  253. // probe for metadata
  254. $table = [];
  255. if(isset($result["rich"]["data"]["rating"])){
  256. $table["Rating"] = $result["rich"]["data"]["rating"];
  257. if(isset($result["rich"]["data"]["num_rating"])){
  258. $table["Rating"] .= " (" . number_format($result["rich"]["data"]["num_rating"]) . " ratings)";
  259. }
  260. }
  261. if(isset($result["rich"]["data"]["views"])){
  262. $table["Views"] = number_format($result["rich"]["data"]["views"]);
  263. }
  264. if(isset($result["rich"]["data"]["duration"])){
  265. $table["Duration"] = $this->int2hms($result["rich"]["data"]["duration"]);
  266. }
  267. if(isset($result["rich"]["data"]["channel_name"])){
  268. $table["Author"] = $result["rich"]["data"]["channel_name"];
  269. }
  270. if(isset($result["rich"]["data"]["video_quality"])){
  271. $table["Quality"] = $result["rich"]["data"]["video_quality"];
  272. }
  273. if(isset($result["rich"]["data"]["category"])){
  274. $table["Category"] = $result["rich"]["data"]["category"];
  275. }
  276. $out["web"][] = [
  277. "title" =>
  278. $this->titledots(
  279. $this->fuckhtml
  280. ->getTextContent(
  281. $result["title"]
  282. )
  283. ),
  284. "description" =>
  285. $this->titledots(
  286. $this->fuckhtml
  287. ->getTextContent(
  288. $result["content"]
  289. )
  290. ),
  291. "url" => $result["url"],
  292. "date" => $date,
  293. "type" => "web",
  294. "thumb" => $thumb,
  295. "sublink" => $sublinks,
  296. "table" => $table
  297. ];
  298. }
  299. //
  300. // Get wikipedia head
  301. //
  302. if(isset($html["right"])){
  303. foreach($html["right"] as $wiki){
  304. $description = [];
  305. if(isset($wiki["short_intro"])){
  306. $description[] =
  307. [
  308. "type" => "quote",
  309. "value" => $wiki["short_intro"],
  310. ];
  311. }
  312. if(isset($wiki["intro"])){
  313. $description[] =
  314. [
  315. "type" => "text",
  316. "value" => $wiki["intro"],
  317. ];
  318. }
  319. // get table elements
  320. $table = [];
  321. if(isset($wiki["fields"])){
  322. foreach($wiki["fields"] as $element){
  323. $table[$element["title"]] = implode(", ", $element["value"]);
  324. }
  325. }
  326. // get sublinks
  327. $sublinks = [];
  328. if(isset($wiki["website"])){
  329. if(
  330. preg_match(
  331. '/^http/',
  332. $wiki["website"]
  333. ) === 0
  334. ){
  335. $sublinks["Website"] = "https://" . $wiki["website"];
  336. }else{
  337. $sublinks["Website"] = $wiki["website"];
  338. }
  339. }
  340. foreach($wiki["profiles"] as $sitename => $url){
  341. $sitename = explode("_", $sitename);
  342. $sitename = ucfirst($sitename[count($sitename) - 1]);
  343. $sublinks[$sitename] = $url;
  344. }
  345. $out["answer"][] = [
  346. "title" =>
  347. $this->titledots(
  348. $wiki["title"]
  349. ),
  350. "description" => $description,
  351. "url" => null,
  352. "thumb" => isset($wiki["image"]["contentUrl"]) ? $wiki["image"]["contentUrl"] : null,
  353. "table" => $table,
  354. "sublink" => $sublinks
  355. ];
  356. }
  357. }
  358. // get next page
  359. if((int)$html["search"]["page"] < (int)$html["search"]["max_page"]){
  360. // https://coccoc.com/composer?_=1754021153532&p=0&q=zbabduiqwhduwqhdnwq&reqid=bwcAs00q&s=direct&apiV=1
  361. // ^json endpoint, but we can just do &page=2 lol
  362. if(!isset($query["page"])){
  363. $query["page"] = 2;
  364. }else{
  365. $query["page"]++;
  366. }
  367. $out["npt"] =
  368. $this->backend
  369. ->store(
  370. json_encode($query),
  371. "web",
  372. $proxy
  373. );
  374. }
  375. return $out;
  376. }
  377. public function video($get){
  378. //$html = file_get_contents("scraper/coccoc.html");
  379. if($get["npt"]){
  380. [$query, $proxy] =
  381. $this->backend->get(
  382. $get["npt"],
  383. "videos"
  384. );
  385. $query = json_decode($query, true);
  386. }else{
  387. $proxy = $this->backend->get_ip();
  388. $query = [
  389. "query" => $get["s"],
  390. "tbm" => "vid"
  391. ];
  392. // add filters
  393. if($get["nsfw"] == "no"){
  394. $query["safe"] = 1;
  395. }
  396. if($get["time"] != "any"){
  397. $query["tbs"] = $get["time"];
  398. }
  399. if($get["filter"] == "yes"){
  400. $query["filter"] = 0;
  401. }
  402. }
  403. try{
  404. $html =
  405. $this->get(
  406. $proxy,
  407. "https://coccoc.com/search",
  408. $query
  409. );
  410. }catch(Exception $error){
  411. throw new Exception("Failed to get search page");
  412. }
  413. $html = explode("window.composerResponse", $html, 2);
  414. if(count($html) !== 2){
  415. throw new Exception("Failed to grep window.composerResponse");
  416. }
  417. $html =
  418. json_decode(
  419. $this->fuckhtml
  420. ->extract_json(
  421. ltrim($html[1], " =")
  422. ),
  423. true
  424. );
  425. if($html === null){
  426. throw new Exception("Failed to decode JSON");
  427. }
  428. $out = [
  429. "status" => "ok",
  430. "npt" => null,
  431. "video" => [],
  432. "author" => [],
  433. "livestream" => [],
  434. "playlist" => [],
  435. "reel" => []
  436. ];
  437. if(!isset($html["search_video"]["search_results"])){
  438. if(isset($html["search_video"]["error"]["title"])){
  439. if($html["search_video"]["error"]["title"] == "Không tìm thấy kết quả nào"){
  440. return $out;
  441. }
  442. throw new Exception("Coc Coc returned an error: " . $html["search_video"]["error"]["title"]);
  443. }
  444. throw new Exception("Coc Coc did not supply a search_results object");
  445. }
  446. foreach($html["search_video"]["search_results"] as $video){
  447. if(isset($video["rich"]["data"]["image_url"])){
  448. $thumb = [
  449. "ratio" => "16:9",
  450. "url" => $video["rich"]["data"]["image_url"]
  451. ];
  452. }else{
  453. $thumb = [
  454. "ratio" => null,
  455. "url" => null
  456. ];
  457. }
  458. $out["video"][] = [
  459. "title" =>
  460. $this->titledots(
  461. $this->fuckhtml
  462. ->getTextContent(
  463. $video["title"]
  464. )
  465. ),
  466. "description" =>
  467. $this->titledots(
  468. $this->fuckhtml
  469. ->getTextContent(
  470. $video["content"]
  471. )
  472. ),
  473. "author" => [
  474. "name" =>
  475. isset($video["rich"]["data"]["channel_name"]) ?
  476. $video["rich"]["data"]["channel_name"] : null,
  477. "url" => null,
  478. "avatar" => null
  479. ],
  480. "date" =>
  481. isset($video["date"]) ?
  482. $video["date"] : null,
  483. "duration" =>
  484. isset($video["rich"]["data"]["duration"]) ?
  485. (int)$video["rich"]["data"]["duration"] : null,
  486. "views" => null,
  487. "thumb" => $thumb,
  488. "url" => $video["url"]
  489. ];
  490. }
  491. // get next page
  492. if((int)$html["search_video"]["page"] < (int)$html["search_video"]["max_page"]){
  493. if(!isset($query["page"])){
  494. $query["page"] = 2;
  495. }else{
  496. $query["page"]++;
  497. }
  498. $out["npt"] =
  499. $this->backend
  500. ->store(
  501. json_encode($query),
  502. "videos",
  503. $proxy
  504. );
  505. }
  506. return $out;
  507. }
  508. private function titledots($title){
  509. return trim($title, " .\t\n\r\0\x0B…");
  510. }
  511. private function int2hms($seconds){
  512. $hours = floor($seconds / 3600);
  513. $minutes = floor(($seconds % 3600) / 60);
  514. $seconds = $seconds % 60;
  515. return sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds);
  516. }
  517. }