1
0

yt.php 26 KB

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