1
0

yt.php 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439
  1. <?php
  2. class yt{
  3. public function __construct(){
  4. include "lib/backend.php";
  5. $this->backend = new backend("yt");
  6. include "lib/fuckhtml.php";
  7. $this->fuckhtml = new fuckhtml();
  8. }
  9. public function getfilters($page){
  10. if($page != "videos"){
  11. return [];
  12. }
  13. return [
  14. "date" => [
  15. "display" => "Time posted",
  16. "option" => [
  17. "any" => "Any time",
  18. "hour" => "Last hour",
  19. "today" => "Today",
  20. "week" => "This week",
  21. "month" => "This month",
  22. "year" => "This year"
  23. ]
  24. ],
  25. "type" => [
  26. "display" => "Type",
  27. "option" => [
  28. "video" => "Video",
  29. "channel" => "Channel",
  30. "playlist" => "Playlist",
  31. "Movie" => "Movie"
  32. ]
  33. ],
  34. "duration" => [
  35. "display" => "Duration",
  36. "option" => [
  37. "any" => "Any duration",
  38. "short" => "Short (>4min)",
  39. "medium" => "Medium (4-20min)",
  40. "long" => "Long (<20min)"
  41. ]
  42. ],
  43. "feature" => [
  44. "display" => "Feature",
  45. "option" => [
  46. "any" => "No features",
  47. "live" => "Live",
  48. "4k" => "4K",
  49. "hd" => "HD",
  50. "subtitles" => "Subtitles/CC",
  51. "creativecommons" => "Creative Commons",
  52. "360" => "VR 360°",
  53. "vr180" => "VR 180°",
  54. "3d" => "3D",
  55. "hdr" => "HDR"
  56. ]
  57. ],
  58. "sort" => [
  59. "display" => "Sort by",
  60. "option" => [
  61. "relevance" => "Relevance",
  62. "upload_date" => "Upload date",
  63. "view_count" => "View count",
  64. "rating" => "Rating"
  65. ]
  66. ]
  67. ];
  68. }
  69. private function ytfilter($date, $type, $duration, $feature, $sort){
  70. // ------------
  71. // INCOMPATIBLE FILTERS
  72. // channel,playlist DURATION, FEATURES, SORT BY
  73. // Movie Features=[live, subtitles, creative commons, 3d]
  74. // live, 3D
  75. // Type[channel, playlist, movie]
  76. // UPLOAD DATE, DURATION, 4k, 360, VR180, HDR
  77. // Type[channel, playlist]
  78. // -----------
  79. // MUST BE TOGETHER
  80. // Relevance,upload date Type=Video
  81. switch($type){
  82. case "channel":
  83. case "playlist":
  84. if($duration != "any"){ $duration = "any"; }
  85. if($feature != "any"){ $feature = "any"; }
  86. if($sort != "any"){ $sort = "any"; }
  87. break;
  88. case "movie":
  89. if(
  90. in_array(
  91. $feature,
  92. [
  93. "live",
  94. "subtitles",
  95. "creative_commons",
  96. "3d"
  97. ],
  98. )
  99. ){
  100. $feature = "any";
  101. }
  102. break;
  103. }
  104. switch($feature){
  105. case "live":
  106. case "3d":
  107. if(
  108. in_array(
  109. $type,
  110. [
  111. "channel",
  112. "playlist",
  113. "movie"
  114. ],
  115. )
  116. ){
  117. $type = "video";
  118. }
  119. break;
  120. }
  121. if(
  122. (
  123. $date != "any" ||
  124. $duration != "any" ||
  125. $feature == "4k" ||
  126. $feature == "360" ||
  127. $feature == "vr180" ||
  128. $feature == "hdr"
  129. ) &&
  130. (
  131. $type == "channel" ||
  132. $type == "playlist"
  133. )
  134. ){
  135. $type = "video";
  136. }
  137. if(
  138. $date == "any" &&
  139. $type == "video" &&
  140. $duration == "any" &&
  141. $feature == "any" &&
  142. $sort == "relevance"
  143. ){
  144. return null;
  145. }
  146. //print_r([$date, $type, $duration, $feature, $sort]);
  147. /*
  148. Encode hex data
  149. */
  150. // UPLOAD DATE
  151. // hour EgQIARAB 12 04 08 01 10 01
  152. // today EgQIAhAB 12 04 08 02 10 01
  153. // week EgQIAxAB 12 04 08 03 10 01
  154. // month EgQIBBAB 12 04 08 04 10 01
  155. // year EgQIBRAB 12 04 08 05 10 01
  156. // TYPE
  157. // video EgIQAQ%253D%253D 12 02 10 01
  158. // channel EgIQAg%253D%253D 12 02 10 02
  159. // playlist EgIQAw%253D%253D 12 02 10 03
  160. // movie EgIQBA%253D%253D 12 02 10 04
  161. // DURATION
  162. // -4min EgIYAQ%253D%253D 12 02 18 01
  163. // 4-20min EgIYAw%253D%253D 12 02 18 03
  164. // 20+min EgIYAg%253D%253D 12 02 18 02
  165. // FEATURE
  166. // live EgJAAQ%253D%253D 12 02 40 01
  167. // 4K EgJwAQ%253D%253D 12 02 70 01
  168. // HD EgIgAQ%253D%253D 12 02 20 01
  169. // Subtitles/CC EgIoAQ%253D%253D 12 02 28 01
  170. // Creative Commons EgIwAQ%253D%253D 12 02 30 01
  171. // 360 EgJ4AQ%253D%253D 12 02 78 01
  172. // VR180 EgPQAQE%253D 12 03 d0 01 01
  173. // 3D EgI4AQ%253D%253D 12 02 38 01
  174. // HDR EgPIAQE%253D 12 03 c8 01 01
  175. // (location & purchased unused)
  176. // SORT BY
  177. // Relevance CAASAhAB 08 00 12 02 10 01 (is nothing by default)
  178. // Upload date CAI%253D 08 02
  179. // View count CAM%253D 08 03
  180. // Rating CAE%253D 08 01
  181. // video
  182. // 12 02 10 01
  183. // under 4 minutes
  184. // 12 02 18 01
  185. // video + under 4 minutes
  186. // 12 04 10 01 18 01
  187. // video + under 4 minutes + HD
  188. // 08 00 12 06 10 01 18 01 20 01
  189. // video + under 4 minutes + upload date
  190. // 08 02 12 04 10 01 18 01
  191. // video + under 4 minutes + HD + upload date
  192. // 08 02 12 06 10 01 18 01 20 01
  193. // this year + video + under 4 minutes + HD + upload date
  194. // 08 02 12 08 08 05 10 01 18 01 20 01
  195. // this week + video + over 20 minutes + HD + view count
  196. // 08 03 12 08 08 03 10 01 18 02 20 01
  197. //echo urlencode(urlencode(base64_encode(hex2bin($str))));
  198. //echo bin2hex(base64_decode(urldecode(urldecode("CAI%253D"))));
  199. // week + video + 20min + rating
  200. // 08 01 12 06 08 03 10 01 18 02
  201. // week + video + 20min + live + rating
  202. // 08 01 12 08 08 03 10 01 18 02 40 01
  203. // live 12 02 40 01
  204. $hex = null;
  205. if(
  206. $date == "any" &&
  207. $type == "video" &&
  208. $duration == "any" &&
  209. $feature == "any" &&
  210. $sort == "relevance"
  211. ){
  212. return $hex;
  213. }
  214. $opcode = 0;
  215. if($date != "any"){ $opcode += 2; }
  216. if($type != "any"){ $opcode += 2; }
  217. if($duration != "any"){ $opcode += 2; }
  218. switch($feature){
  219. case "live":
  220. case "4k":
  221. case "hd":
  222. case "subtitles":
  223. case "creativecommons":
  224. case "360":
  225. case "3d":
  226. $opcode += 2;
  227. break;
  228. case "hdr":
  229. case "vr180":
  230. $opcode += 3;
  231. break;
  232. }
  233. switch($sort){
  234. case "relevance": $hex .= "0800"; break;
  235. case "upload_date": $hex .= "0802"; break;
  236. case "view_count": $hex .= "0803"; break;
  237. case "rating": $hex .= "0801"; break;
  238. }
  239. $hex .= "12" . "0".$opcode;
  240. switch($date){
  241. case "hour": $hex .= "0801"; break;
  242. case "today": $hex .= "0802"; break;
  243. case "week": $hex .= "0803"; break;
  244. case "month": $hex .= "0804"; break;
  245. case "year": $hex .= "0805"; break;
  246. }
  247. switch($type){
  248. case "video": $hex .= "1001"; break;
  249. case "channel": $hex .= "1002"; break;
  250. case "playlist": $hex .= "1003"; break;
  251. case "movie": $hex .= "1004"; break;
  252. }
  253. switch($duration){
  254. case "short": $hex .= "1801"; break;
  255. case "medium": $hex .= "1803"; break;
  256. case "long": $hex .= "1802"; break;
  257. }
  258. switch($feature){
  259. case "live": $hex .= "4001"; break;
  260. case "4k": $hex .= "7001"; break;
  261. case "hd": $hex .= "2001"; break;
  262. case "subtitles": $hex .= "2801"; break;
  263. case "creativecommons": $hex .= "3001"; break;
  264. case "360": $hex .= "7801"; break;
  265. case "vr180": $hex .= "d00101"; break;
  266. case "3d": $hex .= "3801"; break;
  267. case "hdr": $hex .= "c80101"; break;
  268. }
  269. //echo $hex . "\n\n";
  270. return urlencode(base64_encode(hex2bin($hex)));
  271. }
  272. // me reading youtube's json
  273. // https://imgur.com/X9hVlFX
  274. const req_web = 0;
  275. const req_xhr = 1;
  276. private function get($proxy, $url, $get = [], $reqtype = self::req_web, $continuation = null){
  277. $curlproc = curl_init();
  278. if($get !== []){
  279. $get = http_build_query($get);
  280. $url .= "?" . $get;
  281. }
  282. curl_setopt($curlproc, CURLOPT_URL, $url);
  283. switch($reqtype){
  284. case self::req_web:
  285. $headers =
  286. ["User-Agent: " . config::USER_AGENT,
  287. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
  288. "Accept-Language: en-US,en;q=0.5",
  289. "Accept-Encoding: gzip, deflate, br, zstd",
  290. "Cookie: PREF=tz=America.New_York",
  291. "DNT: 1",
  292. "Connection: keep-alive",
  293. "Upgrade-Insecure-Requests: 1",
  294. "Sec-Fetch-Dest: document",
  295. "Sec-Fetch-Mode: navigate",
  296. "Sec-Fetch-Site: none",
  297. "Sec-Fetch-User: ?1"];
  298. break;
  299. case self::req_xhr:
  300. $headers =
  301. ["User-Agent: " . config::USER_AGENT,
  302. "Accept: */*",
  303. "Accept-Language: en-US,en;q=0.5",
  304. "Accept-Encoding: gzip, deflate, br, zstd",
  305. "Cookie: PREF=tz=America.New_York",
  306. "Referer: https://youtube.com.com/",
  307. "Content-Type: application/json",
  308. "Content-Length: " . strlen($continuation),
  309. "DNT: 1",
  310. "Connection: keep-alive",
  311. "Sec-Fetch-Dest: empty",
  312. "Sec-Fetch-Mode: same-origin",
  313. "Sec-Fetch-Site: same-origin"];
  314. curl_setopt($curlproc, CURLOPT_POST, true);
  315. curl_setopt($curlproc, CURLOPT_POSTFIELDS, $continuation);
  316. break;
  317. }
  318. curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
  319. curl_setopt($curlproc, CURLOPT_HTTPHEADER, $headers);
  320. curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
  321. curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
  322. curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
  323. curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
  324. curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
  325. $this->backend->assign_proxy($curlproc, $proxy);
  326. $data = curl_exec($curlproc);
  327. if(curl_errno($curlproc)){
  328. throw new Exception(curl_error($curlproc));
  329. }
  330. curl_close($curlproc);
  331. return $data;
  332. }
  333. public function video($get){
  334. $out = [
  335. "status" => "ok",
  336. "npt" => null,
  337. "video" => [],
  338. "author" => [],
  339. "livestream" => [],
  340. "playlist" => [],
  341. "reel" => []
  342. ];
  343. if($get["npt"]){
  344. $first_page = false;
  345. [$npt, $proxy] =
  346. $this->backend->get(
  347. $get["npt"],
  348. "videos"
  349. );
  350. $npt = json_decode($npt, true);
  351. try{
  352. $json = $this->get(
  353. $proxy,
  354. "https://www.youtube.com/youtubei/v1/search",
  355. [
  356. "key" => $npt["key"],
  357. "prettyPrint" => "false"
  358. ],
  359. self::req_xhr,
  360. json_encode($npt["post"])
  361. );
  362. }catch(Exception $error){
  363. throw new Exception("Could not fetch search page");
  364. }
  365. $json = json_decode($json, true);
  366. }else{
  367. $search = $get["s"];
  368. if(strlen($search) === 0){
  369. throw new Exception("Search term is empty!");
  370. }
  371. $proxy = $this->backend->get_ip();
  372. $date = $get["date"];
  373. $type = $get["type"];
  374. $duration = $get["duration"];
  375. $feature = $get["feature"];
  376. $sort = $get["sort"];
  377. // parse ytInitialData
  378. $get = [
  379. "search_query" => $search
  380. ];
  381. if(
  382. (
  383. $filter =
  384. $this->ytfilter(
  385. $date,
  386. $type,
  387. $duration,
  388. $feature,
  389. $sort
  390. )
  391. ) !== null
  392. ){
  393. $get["sp"] = $filter;
  394. }
  395. try{
  396. $json = $this->get(
  397. $proxy,
  398. "https://www.youtube.com/results",
  399. $get
  400. );
  401. }catch(Exception $error){
  402. throw new Exception("Could not fetch search page");
  403. }
  404. $this->detect_sorry($json);
  405. //$json = file_get_contents("scraper/yt.html");
  406. // get next page data, unique to first page
  407. if(
  408. !preg_match(
  409. '/ytcfg\.set\(({".*})\); *window\.ytcfg/',
  410. $json,
  411. $ytconfig
  412. )
  413. ){
  414. throw new Exception("Could not get ytcfg");
  415. }
  416. $ytconfig = json_decode($ytconfig[1], true);
  417. $first_page = true;
  418. // this is where all of the data is
  419. if(
  420. !preg_match(
  421. '/ytInitialData *= *({.*});<\/script>/',
  422. $json,
  423. $json
  424. )
  425. ){
  426. throw new Exception("Could not get ytInitialData");
  427. }
  428. $json = json_decode($json[1], true);
  429. /*$first_page = true;
  430. $json = json_decode(file_get_contents("scraper/yt.html"), true);*/
  431. }
  432. $items = [
  433. "channelRenderer" => [],
  434. "videoRenderer" => [],
  435. "shortsLockupViewModel" => [],
  436. "lockupViewModel" => [], // playlists
  437. "continuationItemRenderer" => [], // pagination
  438. "backgroundPromoRenderer" => [] // errors
  439. ];
  440. $search = [
  441. "channelRenderer",
  442. "videoRenderer",
  443. "shortsLockupViewModel",
  444. "lockupViewModel",
  445. "continuationItemRenderer",
  446. "backgroundPromoRenderer"
  447. ];
  448. $this->recursive_search($search, $items, $json);
  449. // check for errors
  450. if(count($items["backgroundPromoRenderer"]) !== 0){
  451. foreach($items["backgroundPromoRenderer"] as $popup){
  452. if(
  453. isset(
  454. $popup
  455. ["title"]
  456. ["runs"]
  457. ) &&
  458. isset(
  459. $popup
  460. ["bodyText"]
  461. ["runs"]
  462. )
  463. ){
  464. $error =
  465. $this->assemble_runs($popup["title"]) . ". " .
  466. $this->assemble_runs($popup["bodyText"]);
  467. if(str_contains(strtolower($error), "no results found")){
  468. // no results
  469. return $out;
  470. }
  471. // other error
  472. throw new Exception(
  473. "YouTube returned an error: " .
  474. $this->assemble_runs($popup["title"]) . ". " .
  475. $this->assemble_runs($popup["bodyText"])
  476. );
  477. }
  478. }
  479. }
  480. // add normal videos
  481. foreach($items["videoRenderer"] as $video){
  482. $show_live_badge = false;
  483. if(
  484. $video["navigationEndpoint"]
  485. ["commandMetadata"]
  486. ["webCommandMetadata"]
  487. ["webPageType"]
  488. == "WEB_PAGE_TYPE_SHORTS"
  489. ){
  490. $type = "reel";
  491. }elseif(isset($video["upcomingEventData"])){
  492. // is an upcoming event, categorize as livestream
  493. $type = "livestream";
  494. }else{
  495. $type = "video";
  496. // detect if its a livestream
  497. if(isset($video["badges"])){
  498. foreach($video["badges"] as $badge){
  499. if(
  500. $badge
  501. ["metadataBadgeRenderer"]
  502. ["label"] == "LIVE"
  503. ){
  504. $type = "livestream";
  505. $show_live_badge = true;
  506. break;
  507. }
  508. }
  509. }
  510. }
  511. $thumbnail =
  512. $video["thumbnail"]
  513. ["thumbnails"]
  514. [0]
  515. ["url"];
  516. $filename = basename(parse_url($thumbnail, PHP_URL_PATH));
  517. if(
  518. $filename == "hq720.jpg" ||
  519. $filename == "mqdefault.jpg"
  520. ){
  521. // fetch trackerless version
  522. // its bigger and consumes more bandwidth, but it wont serve
  523. // blurred out thumbnails
  524. $thumbnail = "https://i.ytimg.com/vi/{$video["videoId"]}/{$filename}";
  525. }
  526. $out[$type][] = [
  527. "title" =>
  528. $this->assemble_runs(
  529. $video
  530. ["title"]
  531. ),
  532. "description" =>
  533. isset(
  534. $video["detailedMetadataSnippets"]
  535. [0]
  536. ["snippetText"]
  537. ) ?
  538. $this->titledots(
  539. $this->assemble_runs(
  540. $video["detailedMetadataSnippets"]
  541. [0]
  542. ["snippetText"]
  543. )
  544. ) : null,
  545. "author" => [
  546. "name" =>
  547. $this->assemble_runs(
  548. $video["longBylineText"]
  549. ),
  550. "url" =>
  551. "https://www.youtube.com" .
  552. $video["ownerText"]
  553. ["runs"]
  554. [0]
  555. ["navigationEndpoint"]
  556. ["commandMetadata"]
  557. ["webCommandMetadata"]
  558. ["url"],
  559. "avatar" =>
  560. $video["channelThumbnailSupportedRenderers"]
  561. ["channelThumbnailWithLinkRenderer"]
  562. ["thumbnail"]
  563. ["thumbnails"]
  564. [0]
  565. ["url"]
  566. ],
  567. "date" =>
  568. isset(
  569. $video["publishedTimeText"]
  570. ["simpleText"]
  571. ) ?
  572. $this->parse_date(
  573. trim(
  574. str_replace(
  575. "Streamed", "",
  576. $video["publishedTimeText"]
  577. ["simpleText"]
  578. )
  579. )
  580. ) : null,
  581. "duration" =>
  582. $show_live_badge === true ?
  583. "_LIVE" :
  584. (
  585. isset(
  586. $video["lengthText"]
  587. ["simpleText"]
  588. ) ?
  589. $this->hms2int(
  590. $video["lengthText"]
  591. ["simpleText"]
  592. ) : null
  593. ),
  594. "views" =>
  595. isset(
  596. $video["viewCountText"]
  597. ["simpleText"]
  598. ) ?
  599. $this->views2int(
  600. $video["viewCountText"]
  601. ["simpleText"]
  602. ) : null,
  603. "thumb" => [
  604. "ratio" => "16:9",
  605. "url" => $thumbnail
  606. ],
  607. "url" => "https://www.youtube.com/watch?v=" . $video["videoId"]
  608. ];
  609. }
  610. // add channels
  611. foreach($items["channelRenderer"] as $channel){
  612. $out["author"][] = [
  613. "title" =>
  614. $channel
  615. ["title"]
  616. ["simpleText"],
  617. "followers" =>
  618. isset(
  619. $channel
  620. ["videoCountText"]
  621. ["simpleText"]
  622. ) ?
  623. $this->truncatedcount2int(
  624. $channel
  625. ["videoCountText"]
  626. ["simpleText"]
  627. ) :
  628. null,
  629. "description" =>
  630. isset($channel["descriptionSnippet"]) ?
  631. $this->titledots(
  632. $this->assemble_runs(
  633. $channel
  634. ["descriptionSnippet"]
  635. )
  636. ) : null,
  637. "thumb" =>
  638. [
  639. "url" =>
  640. $this->checkhttpspresence(
  641. $channel
  642. ["thumbnail"]
  643. ["thumbnails"]
  644. [
  645. count(
  646. $channel
  647. ["thumbnail"]
  648. ["thumbnails"]
  649. ) - 1
  650. ]
  651. ["url"]
  652. ),
  653. "ratio" => "1:1"
  654. ],
  655. "url" =>
  656. "https://www.youtube.com/channel/" .
  657. $channel
  658. ["channelId"]
  659. ];
  660. }
  661. // add reels
  662. foreach($items["shortsLockupViewModel"] as $reel){
  663. $out["reel"][] = [
  664. "title" =>
  665. $reel
  666. ["overlayMetadata"]
  667. ["primaryText"]
  668. ["content"],
  669. "description" =>
  670. isset(
  671. $reel
  672. ["overlayMetadata"]
  673. ["primaryText"]
  674. ["content"]
  675. ) ?
  676. $this->titledots(
  677. $reel
  678. ["overlayMetadata"]
  679. ["primaryText"]
  680. ["content"]
  681. ) : null,
  682. "author" => [
  683. "name" => null,
  684. "url" => null,
  685. "avatar" => null
  686. ],
  687. "date" => null,
  688. "duration" => null,
  689. "views" =>
  690. $this->truncatedcount2int(
  691. $reel
  692. ["overlayMetadata"]
  693. ["secondaryText"]
  694. ["content"]
  695. ),
  696. "thumb" => [
  697. "url" =>
  698. $reel
  699. ["thumbnailViewModel"]
  700. ["thumbnailViewModel"]
  701. ["image"]
  702. ["sources"]
  703. [0]
  704. ["url"],
  705. "ratio" => "9:16"
  706. ],
  707. "url" =>
  708. "https://www.youtube.com/watch?v=" .
  709. $reel
  710. ["onTap"]
  711. ["innertubeCommand"]
  712. ["reelWatchEndpoint"]
  713. ["videoId"]
  714. ];
  715. }
  716. // add playlists
  717. foreach($items["lockupViewModel"] as $playlist){
  718. // get author of playlist
  719. $author_name = null;
  720. $author_url = null;
  721. if(
  722. !isset(
  723. $playlist
  724. ["metadata"]
  725. ["lockupMetadataViewModel"]
  726. ["title"]
  727. ["content"]
  728. ) ||
  729. !isset(
  730. $playlist
  731. ["itemPlayback"]
  732. ["inlinePlayerData"]
  733. ["onSelect"]
  734. ["innertubeCommand"]
  735. ["commandMetadata"]
  736. ["webCommandMetadata"]
  737. ["url"]
  738. )
  739. ){
  740. // got an ad viewmodel or some invalid item with no link
  741. // i dont know why the fuck that happens dont ask me
  742. continue;
  743. }
  744. foreach(
  745. $playlist
  746. ["metadata"]
  747. ["lockupMetadataViewModel"]
  748. ["metadata"]
  749. ["contentMetadataViewModel"]
  750. ["metadataRows"]
  751. [0]
  752. ["metadataParts"]
  753. as $possible_author
  754. ){
  755. if(
  756. isset(
  757. $possible_author
  758. ["text"]
  759. ["commandRuns"]
  760. [0]
  761. ["onTap"]
  762. ["innertubeCommand"]
  763. ["commandMetadata"]
  764. ["webCommandMetadata"]
  765. ["webPageType"]
  766. ) &&
  767. $possible_author
  768. ["text"]
  769. ["commandRuns"]
  770. [0]
  771. ["onTap"]
  772. ["innertubeCommand"]
  773. ["commandMetadata"]
  774. ["webCommandMetadata"]
  775. ["webPageType"] == "WEB_PAGE_TYPE_CHANNEL"
  776. ){
  777. $author_name = $possible_author["text"]["content"];
  778. $author_url =
  779. "https://www.youtube.com" .
  780. $possible_author
  781. ["text"]
  782. ["commandRuns"]
  783. [0]
  784. ["onTap"]
  785. ["innertubeCommand"]
  786. ["commandMetadata"]
  787. ["webCommandMetadata"]
  788. ["url"];
  789. break;
  790. }
  791. }
  792. $out["playlist"][] = [
  793. "title" =>
  794. $playlist
  795. ["metadata"]
  796. ["lockupMetadataViewModel"]
  797. ["title"]
  798. ["content"],
  799. "description" => null,
  800. "author" => [
  801. "name" => $author_name,
  802. "url" => $author_url,
  803. "avatar" => null
  804. ],
  805. "date" => null,
  806. "duration" => null,
  807. "views" => null,
  808. "thumb" => [
  809. "url" =>
  810. $playlist
  811. ["contentImage"]
  812. ["collectionThumbnailViewModel"]
  813. ["primaryThumbnail"]
  814. ["thumbnailViewModel"]
  815. ["image"]
  816. ["sources"]
  817. [0]
  818. ["url"],
  819. "ratio" => "16:9"
  820. ],
  821. "url" =>
  822. "https://www.youtube.com" .
  823. $playlist
  824. ["itemPlayback"]
  825. ["inlinePlayerData"]
  826. ["onSelect"]
  827. ["innertubeCommand"]
  828. ["commandMetadata"]
  829. ["webCommandMetadata"]
  830. ["url"]
  831. ];
  832. }
  833. // get next page
  834. $found_npt = false;
  835. if($first_page === true){
  836. // we're using the old pagination trick, google now sends encrypted data
  837. if(count($items["continuationItemRenderer"]) !== 0){
  838. $ytconfig["INNERTUBE_CONTEXT"]["client"]["screenWidthPoints"] = 1920;
  839. $ytconfig["INNERTUBE_CONTEXT"]["client"]["screenHeightPoints"] = 999;
  840. $ytconfig["INNERTUBE_CONTEXT"]["client"]["screenPixelDensity"] = 1;
  841. $ytconfig["INNERTUBE_CONTEXT"]["client"]["screenDensityFloat"] = 1;
  842. $ytconfig["INNERTUBE_CONTEXT"]["client"]["utcOffsetMinutes"] = -240;
  843. $ytconfig["INNERTUBE_CONTEXT"]["client"]["internalExperimentFlags"] = [];
  844. $ytconfig["INNERTUBE_CONTEXT"]["client"]["concistencyTokenJars"] = [];
  845. $ytconfig["INNERTUBE_CONTEXT"]["client"]["mainAppWebInfo"] = [
  846. "graftUrl" => $ytconfig["INNERTUBE_CONTEXT"]["client"]["originalUrl"],
  847. "webDisplayMode" => "WEB_DISPLAY_MODE_BROWSER",
  848. "isWebNativeShareAvailable" => false
  849. ];
  850. $ytconfig["INNERTUBE_CONTEXT"]["adSignalsInfo"] = [
  851. "params" => [
  852. [
  853. "key" => "dt",
  854. "value" => (string)$ytconfig["TIME_CREATED_MS"]
  855. ],
  856. [
  857. "key" => "flash",
  858. "value" => "0"
  859. ],
  860. [
  861. "key" => "frm",
  862. "value" => "0"
  863. ],
  864. [
  865. "key" => "u_tz",
  866. "value" => "-240"
  867. ],
  868. [
  869. "key" => "u_his",
  870. "value" => "3"
  871. ],
  872. [
  873. "key" => "u_h",
  874. "value" => "1080"
  875. ],
  876. [
  877. "key" => "u_w",
  878. "value" => "1920"
  879. ],
  880. [
  881. "key" => "u_ah",
  882. "value" => "1080"
  883. ],
  884. [
  885. "key" => "u_cd",
  886. "value" => "24"
  887. ],
  888. [
  889. "key" => "bc",
  890. "value" => "31"
  891. ],
  892. [
  893. "key" => "bih",
  894. "value" => "999"
  895. ],
  896. [
  897. "key" => "biw",
  898. "value" => "1920"
  899. ],
  900. [
  901. "key" => "brdim",
  902. "value" => "0,0,0,0,1920,0,1920,1061,1920,999"
  903. ],
  904. [
  905. "key" => "vis",
  906. "value" => "1"
  907. ],
  908. [
  909. "key" => "wgl",
  910. "value" => "true"
  911. ],
  912. [
  913. "key" => "ca_type",
  914. "value" => "image"
  915. ]
  916. ]
  917. ];
  918. $found_npt = true;
  919. $npt = [
  920. "key" =>
  921. $ytconfig
  922. ["INNERTUBE_API_KEY"],
  923. "post" => [
  924. "context" =>
  925. $ytconfig
  926. ["INNERTUBE_CONTEXT"],
  927. "continuation" =>
  928. $items
  929. ["continuationItemRenderer"]
  930. [0]
  931. ["continuationEndpoint"]
  932. ["continuationCommand"]
  933. ["token"]
  934. ]
  935. ];
  936. }
  937. }else{
  938. // handle next page for json endpoint
  939. if(count($items["continuationItemRenderer"]) !== 0){
  940. $found_npt = true;
  941. $npt["post"]["continuation"] =
  942. $items
  943. ["continuationItemRenderer"]
  944. [0]
  945. ["continuationEndpoint"]
  946. ["continuationCommand"]
  947. ["token"];
  948. }
  949. }
  950. if($found_npt){
  951. $out["npt"] =
  952. $this->backend->store(
  953. json_encode(
  954. $npt
  955. ),
  956. "videos",
  957. $proxy
  958. );
  959. }
  960. return $out;
  961. }
  962. private function recursive_search(&$search, &$items, &$pointer){
  963. foreach($pointer as $key => $value){
  964. if(
  965. in_array(
  966. $key,
  967. $search
  968. )
  969. ){
  970. // found interesting item
  971. $items[$key][] = $value;
  972. }elseif(is_array($value)){
  973. $this->recursive_search($search, $items, $value);
  974. }
  975. }
  976. }
  977. private function textualdate2unix($number){
  978. $number =
  979. explode(
  980. " ",
  981. str_replace(
  982. [
  983. " ago",
  984. "seconds",
  985. "minutes",
  986. "hours",
  987. "days",
  988. "weeks",
  989. "months",
  990. "years"
  991. ],
  992. [
  993. "",
  994. "second",
  995. "minute",
  996. "hour",
  997. "day",
  998. "week",
  999. "month",
  1000. "year"
  1001. ],
  1002. $number
  1003. ),
  1004. 2
  1005. );
  1006. $time = 0;
  1007. switch($number[1]){
  1008. case "second":
  1009. $time = (int)$number[0];
  1010. break;
  1011. case "minute":
  1012. $time = (int)$number[0] * 60;
  1013. break;
  1014. case "hour":
  1015. $time = (int)$number[0] * 3600;
  1016. break;
  1017. case "day":
  1018. $time = (int)$number[0] * 86400;
  1019. break;
  1020. case "week":
  1021. $time = (int)$number[0] * 604800;
  1022. break;
  1023. case "month":
  1024. $time = (int)$number[0] * 2629746;
  1025. break;
  1026. case "year":
  1027. $time = (int)$number[0] * 31556952;
  1028. break;
  1029. }
  1030. return time() - $time;
  1031. }
  1032. private function checkhttpspresence($link){
  1033. if(substr($link, 0, 2) == "//"){
  1034. return "https:" . $link;
  1035. }
  1036. return $link;
  1037. }
  1038. private function textualtime2int($number){
  1039. $number = explode(" - ", $number);
  1040. if(count($number) >= 2){
  1041. $number = $number[count($number) - 2];
  1042. }else{
  1043. $number = $number[0];
  1044. }
  1045. $number =
  1046. str_replace(
  1047. [
  1048. " ",
  1049. "seconds",
  1050. "minutes",
  1051. "hours",
  1052. ],
  1053. [
  1054. "",
  1055. "second",
  1056. "minute",
  1057. "hour"
  1058. ],
  1059. $number
  1060. );
  1061. preg_match_all(
  1062. '/([0-9]+)(second|minute|hour)/',
  1063. $number,
  1064. $number
  1065. );
  1066. $time = 0;
  1067. for($i=0; $i<count($number[0]); $i++){
  1068. switch($number[2][$i]){
  1069. case "second":
  1070. $time = $time + (int)$number[1][$i];
  1071. break;
  1072. case "minute":
  1073. $time = $time + ((int)$number[1][$i] * 60);
  1074. break;
  1075. case "hour":
  1076. $time = $time + ((int)$number[1][$i] * 3600);
  1077. break;
  1078. }
  1079. }
  1080. return $time;
  1081. }
  1082. private function views2int($views){
  1083. return
  1084. (int)str_replace(
  1085. ",", "",
  1086. explode(" ", $views, 2)[0]
  1087. );
  1088. }
  1089. private function hms2int($time){
  1090. $parts = explode(":", $time, 3);
  1091. $time = 0;
  1092. if(count($parts) === 3){
  1093. // hours
  1094. $time = $time + ((int)$parts[0] * 3600);
  1095. array_shift($parts);
  1096. }
  1097. if(count($parts) === 2){
  1098. // minutes
  1099. $time = $time + ((int)$parts[0] * 60);
  1100. array_shift($parts);
  1101. }
  1102. // seconds
  1103. $time = $time + (int)$parts[0];
  1104. return $time;
  1105. }
  1106. private function truncatedcount2int($number){
  1107. // decimal should always be 1 number long
  1108. $number = explode(" ", $number, 2);
  1109. $number = $number[0];
  1110. $unit = strtolower($number[strlen($number) - 1]);
  1111. $tmp = explode(".", $number, 2);
  1112. $number = (int)$number;
  1113. if(count($tmp) === 2){
  1114. $decimal = (int)$tmp[1];
  1115. }else{
  1116. $decimal = 0;
  1117. }
  1118. switch($unit){
  1119. case "k":
  1120. $exponant = 1000;
  1121. break;
  1122. case "m":
  1123. $exponant = 1000000;
  1124. break;
  1125. case "b";
  1126. $exponant = 1000000000;
  1127. break;
  1128. default:
  1129. $exponant = 1;
  1130. break;
  1131. }
  1132. return ($number * $exponant) + ($decimal * ($exponant / 10));
  1133. }
  1134. private function titledots($title){
  1135. $substr = substr($title, -3);
  1136. if(
  1137. $substr == "..." ||
  1138. $substr == "…"
  1139. ){
  1140. return str_replace(["\r", "\n"], " ", trim(substr($title, 0, -3), " \n\r\t\v\x00\0\x0B\xc2\xa0"));
  1141. }
  1142. return str_replace(["\r", "\n"], " ", trim($title, " \n\r\t\v\x00\0\x0B\xc2\xa0"));
  1143. }
  1144. private function parse_date($date){
  1145. if(
  1146. preg_match(
  1147. '/^([0-9]+)(s|m|h|d|w|mo|y) ago$/',
  1148. $date,
  1149. $control_chars
  1150. )
  1151. ){
  1152. switch($control_chars[2]){
  1153. case "s": $selection = "seconds"; break;
  1154. case "m": $selection = "minutes"; break;
  1155. case "h": $selection = "hours"; break;
  1156. case "d": $selection = "days"; break;
  1157. case "w": $selection = "weeks"; break;
  1158. case "mo": $selection = "months"; break;
  1159. case "y": $selection = "years"; break;
  1160. default: return null;
  1161. }
  1162. $date = "{$control_chars[1]} {$selection} ago";
  1163. }
  1164. $out = strtotime($date);
  1165. if($out === false){ $out = null; }
  1166. return $out;
  1167. }
  1168. private function assemble_runs($runs){
  1169. if(!isset($runs["runs"])){ return ""; }
  1170. $text = "";
  1171. foreach($runs["runs"] as $run){
  1172. if(isset($run["text"])){
  1173. $text .= $run["text"];
  1174. }
  1175. }
  1176. return $text;
  1177. }
  1178. private function detect_sorry($html){
  1179. $this->fuckhtml->load($html);
  1180. $as =
  1181. $this->fuckhtml
  1182. ->getElementsByTagName(
  1183. "a"
  1184. );
  1185. foreach($as as $a){
  1186. // https://www.google.com/sorry/index?continue=https://www.youtube.com/results%3Fsearch_query%3Dhigurashi&q=EhAmBm0ADxn8AAAAAAAAAAEBGOuP_tQGIjDfNp10wvoKdz1WVrgH0F9RjWm2DrJNrPEJl642dSbds6QXWka0rtTlh2s8o0_c5nwyBz5qY25kclJaAUM
  1187. if(
  1188. stripos(
  1189. $this->fuckhtml
  1190. ->getTextContent(
  1191. $a["attributes"]["href"]
  1192. ),
  1193. "https://www.google.com/sorry/index?continue="
  1194. ) !== false
  1195. ){
  1196. throw new Exception("YouTube returned a captcha");
  1197. }
  1198. }
  1199. }
  1200. }