yt.php 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405
  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. /*$first_page = true;
  427. $json = json_decode(file_get_contents("scraper/yt.html"), true);*/
  428. }
  429. $items = [
  430. "channelRenderer" => [],
  431. "videoRenderer" => [],
  432. "shortsLockupViewModel" => [],
  433. "lockupViewModel" => [], // playlists
  434. "continuationItemRenderer" => [], // pagination
  435. "backgroundPromoRenderer" => [] // errors
  436. ];
  437. $search = [
  438. "channelRenderer",
  439. "videoRenderer",
  440. "shortsLockupViewModel",
  441. "lockupViewModel",
  442. "continuationItemRenderer",
  443. "backgroundPromoRenderer"
  444. ];
  445. $this->recursive_search($search, $items, $json);
  446. // check for errors
  447. if(count($items["backgroundPromoRenderer"]) !== 0){
  448. foreach($items["backgroundPromoRenderer"] as $popup){
  449. if(
  450. isset(
  451. $popup
  452. ["title"]
  453. ["runs"]
  454. ) &&
  455. isset(
  456. $popup
  457. ["bodyText"]
  458. ["runs"]
  459. )
  460. ){
  461. $error =
  462. $this->assemble_runs($popup["title"]) . ". " .
  463. $this->assemble_runs($popup["bodyText"]);
  464. if(str_contains(strtolower($error), "no results found")){
  465. // no results
  466. return $out;
  467. }
  468. // other error
  469. throw new Exception(
  470. "YouTube returned an error: " .
  471. $this->assemble_runs($popup["title"]) . ". " .
  472. $this->assemble_runs($popup["bodyText"])
  473. );
  474. }
  475. }
  476. }
  477. // add normal videos
  478. foreach($items["videoRenderer"] as $video){
  479. $show_live_badge = false;
  480. if(
  481. $video["navigationEndpoint"]
  482. ["commandMetadata"]
  483. ["webCommandMetadata"]
  484. ["webPageType"]
  485. == "WEB_PAGE_TYPE_SHORTS"
  486. ){
  487. $type = "reel";
  488. }elseif(isset($video["upcomingEventData"])){
  489. // is an upcoming event, categorize as livestream
  490. $type = "livestream";
  491. }else{
  492. $type = "video";
  493. // detect if its a livestream
  494. if(isset($video["badges"])){
  495. foreach($video["badges"] as $badge){
  496. if(
  497. $badge
  498. ["metadataBadgeRenderer"]
  499. ["label"] == "LIVE"
  500. ){
  501. $type = "livestream";
  502. $show_live_badge = true;
  503. break;
  504. }
  505. }
  506. }
  507. }
  508. $thumbnail =
  509. $video["thumbnail"]
  510. ["thumbnails"]
  511. [0]
  512. ["url"];
  513. $filename = basename(parse_url($thumbnail, PHP_URL_PATH));
  514. if(
  515. $filename == "hq720.jpg" ||
  516. $filename == "mqdefault.jpg"
  517. ){
  518. // fetch trackerless version
  519. // its bigger and consumes more bandwidth, but it wont serve
  520. // blurred out thumbnails
  521. $thumbnail = "https://i.ytimg.com/vi/{$video["videoId"]}/{$filename}";
  522. }
  523. $out[$type][] = [
  524. "title" =>
  525. $this->assemble_runs(
  526. $video
  527. ["title"]
  528. ),
  529. "description" =>
  530. isset(
  531. $video["detailedMetadataSnippets"]
  532. [0]
  533. ["snippetText"]
  534. ) ?
  535. $this->titledots(
  536. $this->assemble_runs(
  537. $video["detailedMetadataSnippets"]
  538. [0]
  539. ["snippetText"]
  540. )
  541. ) : null,
  542. "author" => [
  543. "name" =>
  544. $this->assemble_runs(
  545. $video["longBylineText"]
  546. ),
  547. "url" =>
  548. "https://www.youtube.com" .
  549. $video["ownerText"]
  550. ["runs"]
  551. [0]
  552. ["navigationEndpoint"]
  553. ["commandMetadata"]
  554. ["webCommandMetadata"]
  555. ["url"],
  556. "avatar" =>
  557. $video["channelThumbnailSupportedRenderers"]
  558. ["channelThumbnailWithLinkRenderer"]
  559. ["thumbnail"]
  560. ["thumbnails"]
  561. [0]
  562. ["url"]
  563. ],
  564. "date" =>
  565. isset(
  566. $video["publishedTimeText"]
  567. ["simpleText"]
  568. ) ?
  569. $this->parse_date(
  570. trim(
  571. str_replace(
  572. "Streamed", "",
  573. $video["publishedTimeText"]
  574. ["simpleText"]
  575. )
  576. )
  577. ) : null,
  578. "duration" =>
  579. $show_live_badge === true ?
  580. "_LIVE" :
  581. (
  582. isset(
  583. $video["lengthText"]
  584. ["simpleText"]
  585. ) ?
  586. $this->hms2int(
  587. $video["lengthText"]
  588. ["simpleText"]
  589. ) : null
  590. ),
  591. "views" =>
  592. isset(
  593. $video["viewCountText"]
  594. ["simpleText"]
  595. ) ?
  596. $this->views2int(
  597. $video["viewCountText"]
  598. ["simpleText"]
  599. ) : null,
  600. "thumb" => [
  601. "ratio" => "16:9",
  602. "url" => $thumbnail
  603. ],
  604. "url" => "https://www.youtube.com/watch?v=" . $video["videoId"]
  605. ];
  606. }
  607. // add channels
  608. foreach($items["channelRenderer"] as $channel){
  609. $out["author"][] = [
  610. "title" =>
  611. $channel
  612. ["title"]
  613. ["simpleText"],
  614. "followers" =>
  615. isset(
  616. $channel
  617. ["videoCountText"]
  618. ["simpleText"]
  619. ) ?
  620. $this->truncatedcount2int(
  621. $channel
  622. ["videoCountText"]
  623. ["simpleText"]
  624. ) :
  625. null,
  626. "description" =>
  627. isset($channel["descriptionSnippet"]) ?
  628. $this->titledots(
  629. $this->assemble_runs(
  630. $channel
  631. ["descriptionSnippet"]
  632. )
  633. ) : null,
  634. "thumb" =>
  635. [
  636. "url" =>
  637. $this->checkhttpspresence(
  638. $channel
  639. ["thumbnail"]
  640. ["thumbnails"]
  641. [
  642. count(
  643. $channel
  644. ["thumbnail"]
  645. ["thumbnails"]
  646. ) - 1
  647. ]
  648. ["url"]
  649. ),
  650. "ratio" => "1:1"
  651. ],
  652. "url" =>
  653. "https://www.youtube.com/channel/" .
  654. $channel
  655. ["channelId"]
  656. ];
  657. }
  658. // add reels
  659. foreach($items["shortsLockupViewModel"] as $reel){
  660. $out["reel"][] = [
  661. "title" =>
  662. $reel
  663. ["overlayMetadata"]
  664. ["primaryText"]
  665. ["content"],
  666. "description" =>
  667. isset(
  668. $reel
  669. ["overlayMetadata"]
  670. ["primaryText"]
  671. ["content"]
  672. ) ?
  673. $this->titledots(
  674. $reel
  675. ["overlayMetadata"]
  676. ["primaryText"]
  677. ["content"]
  678. ) : null,
  679. "author" => [
  680. "name" => null,
  681. "url" => null,
  682. "avatar" => null
  683. ],
  684. "date" => null,
  685. "duration" => null,
  686. "views" =>
  687. $this->truncatedcount2int(
  688. $reel
  689. ["overlayMetadata"]
  690. ["secondaryText"]
  691. ["content"]
  692. ),
  693. "thumb" => [
  694. "url" =>
  695. $reel
  696. ["thumbnailViewModel"]
  697. ["thumbnailViewModel"]
  698. ["image"]
  699. ["sources"]
  700. [0]
  701. ["url"],
  702. "ratio" => "9:16"
  703. ],
  704. "url" =>
  705. "https://www.youtube.com/watch?v=" .
  706. $reel
  707. ["onTap"]
  708. ["innertubeCommand"]
  709. ["reelWatchEndpoint"]
  710. ["videoId"]
  711. ];
  712. }
  713. // add playlists
  714. foreach($items["lockupViewModel"] as $playlist){
  715. // get author of playlist
  716. $author_name = null;
  717. $author_url = null;
  718. if(
  719. !isset(
  720. $playlist
  721. ["metadata"]
  722. ["lockupMetadataViewModel"]
  723. ["title"]
  724. ["content"]
  725. ) ||
  726. !isset(
  727. $playlist
  728. ["itemPlayback"]
  729. ["inlinePlayerData"]
  730. ["onSelect"]
  731. ["innertubeCommand"]
  732. ["commandMetadata"]
  733. ["webCommandMetadata"]
  734. ["url"]
  735. )
  736. ){
  737. // got an ad viewmodel or some invalid item with no link
  738. // i dont know why the fuck that happens dont ask me
  739. continue;
  740. }
  741. foreach(
  742. $playlist
  743. ["metadata"]
  744. ["lockupMetadataViewModel"]
  745. ["metadata"]
  746. ["contentMetadataViewModel"]
  747. ["metadataRows"]
  748. [0]
  749. ["metadataParts"]
  750. as $possible_author
  751. ){
  752. if(
  753. isset(
  754. $possible_author
  755. ["text"]
  756. ["commandRuns"]
  757. [0]
  758. ["onTap"]
  759. ["innertubeCommand"]
  760. ["commandMetadata"]
  761. ["webCommandMetadata"]
  762. ["webPageType"]
  763. ) &&
  764. $possible_author
  765. ["text"]
  766. ["commandRuns"]
  767. [0]
  768. ["onTap"]
  769. ["innertubeCommand"]
  770. ["commandMetadata"]
  771. ["webCommandMetadata"]
  772. ["webPageType"] == "WEB_PAGE_TYPE_CHANNEL"
  773. ){
  774. $author_name = $possible_author["text"]["content"];
  775. $author_url =
  776. "https://www.youtube.com" .
  777. $possible_author
  778. ["text"]
  779. ["commandRuns"]
  780. [0]
  781. ["onTap"]
  782. ["innertubeCommand"]
  783. ["commandMetadata"]
  784. ["webCommandMetadata"]
  785. ["url"];
  786. break;
  787. }
  788. }
  789. $out["playlist"][] = [
  790. "title" =>
  791. $playlist
  792. ["metadata"]
  793. ["lockupMetadataViewModel"]
  794. ["title"]
  795. ["content"],
  796. "description" => null,
  797. "author" => [
  798. "name" => $author_name,
  799. "url" => $author_url,
  800. "avatar" => null
  801. ],
  802. "date" => null,
  803. "duration" => null,
  804. "views" => null,
  805. "thumb" => [
  806. "url" =>
  807. $playlist
  808. ["contentImage"]
  809. ["collectionThumbnailViewModel"]
  810. ["primaryThumbnail"]
  811. ["thumbnailViewModel"]
  812. ["image"]
  813. ["sources"]
  814. [0]
  815. ["url"],
  816. "ratio" => "16:9"
  817. ],
  818. "url" =>
  819. "https://www.youtube.com" .
  820. $playlist
  821. ["itemPlayback"]
  822. ["inlinePlayerData"]
  823. ["onSelect"]
  824. ["innertubeCommand"]
  825. ["commandMetadata"]
  826. ["webCommandMetadata"]
  827. ["url"]
  828. ];
  829. }
  830. // get next page
  831. $found_npt = false;
  832. if($first_page === true){
  833. // we're using the old pagination trick, google now sends encrypted data
  834. if(count($items["continuationItemRenderer"]) !== 0){
  835. $ytconfig["INNERTUBE_CONTEXT"]["client"]["screenWidthPoints"] = 1920;
  836. $ytconfig["INNERTUBE_CONTEXT"]["client"]["screenHeightPoints"] = 999;
  837. $ytconfig["INNERTUBE_CONTEXT"]["client"]["screenPixelDensity"] = 1;
  838. $ytconfig["INNERTUBE_CONTEXT"]["client"]["screenDensityFloat"] = 1;
  839. $ytconfig["INNERTUBE_CONTEXT"]["client"]["utcOffsetMinutes"] = -240;
  840. $ytconfig["INNERTUBE_CONTEXT"]["client"]["internalExperimentFlags"] = [];
  841. $ytconfig["INNERTUBE_CONTEXT"]["client"]["concistencyTokenJars"] = [];
  842. $ytconfig["INNERTUBE_CONTEXT"]["client"]["mainAppWebInfo"] = [
  843. "graftUrl" => $ytconfig["INNERTUBE_CONTEXT"]["client"]["originalUrl"],
  844. "webDisplayMode" => "WEB_DISPLAY_MODE_BROWSER",
  845. "isWebNativeShareAvailable" => false
  846. ];
  847. $ytconfig["INNERTUBE_CONTEXT"]["adSignalsInfo"] = [
  848. "params" => [
  849. [
  850. "key" => "dt",
  851. "value" => (string)$ytconfig["TIME_CREATED_MS"]
  852. ],
  853. [
  854. "key" => "flash",
  855. "value" => "0"
  856. ],
  857. [
  858. "key" => "frm",
  859. "value" => "0"
  860. ],
  861. [
  862. "key" => "u_tz",
  863. "value" => "-240"
  864. ],
  865. [
  866. "key" => "u_his",
  867. "value" => "3"
  868. ],
  869. [
  870. "key" => "u_h",
  871. "value" => "1080"
  872. ],
  873. [
  874. "key" => "u_w",
  875. "value" => "1920"
  876. ],
  877. [
  878. "key" => "u_ah",
  879. "value" => "1080"
  880. ],
  881. [
  882. "key" => "u_cd",
  883. "value" => "24"
  884. ],
  885. [
  886. "key" => "bc",
  887. "value" => "31"
  888. ],
  889. [
  890. "key" => "bih",
  891. "value" => "999"
  892. ],
  893. [
  894. "key" => "biw",
  895. "value" => "1920"
  896. ],
  897. [
  898. "key" => "brdim",
  899. "value" => "0,0,0,0,1920,0,1920,1061,1920,999"
  900. ],
  901. [
  902. "key" => "vis",
  903. "value" => "1"
  904. ],
  905. [
  906. "key" => "wgl",
  907. "value" => "true"
  908. ],
  909. [
  910. "key" => "ca_type",
  911. "value" => "image"
  912. ]
  913. ]
  914. ];
  915. $found_npt = true;
  916. $npt = [
  917. "key" =>
  918. $ytconfig
  919. ["INNERTUBE_API_KEY"],
  920. "post" => [
  921. "context" =>
  922. $ytconfig
  923. ["INNERTUBE_CONTEXT"],
  924. "continuation" =>
  925. $items
  926. ["continuationItemRenderer"]
  927. [0]
  928. ["continuationEndpoint"]
  929. ["continuationCommand"]
  930. ["token"]
  931. ]
  932. ];
  933. }
  934. }else{
  935. // handle next page for json endpoint
  936. if(count($items["continuationItemRenderer"]) !== 0){
  937. $found_npt = true;
  938. $npt["post"]["continuation"] =
  939. $items
  940. ["continuationItemRenderer"]
  941. [0]
  942. ["continuationEndpoint"]
  943. ["continuationCommand"]
  944. ["token"];
  945. }
  946. }
  947. if($found_npt){
  948. $out["npt"] =
  949. $this->backend->store(
  950. json_encode(
  951. $npt
  952. ),
  953. "videos",
  954. $proxy
  955. );
  956. }
  957. return $out;
  958. }
  959. private function recursive_search(&$search, &$items, &$pointer){
  960. foreach($pointer as $key => $value){
  961. if(
  962. in_array(
  963. $key,
  964. $search
  965. )
  966. ){
  967. // found interesting item
  968. $items[$key][] = $value;
  969. }elseif(is_array($value)){
  970. $this->recursive_search($search, $items, $value);
  971. }
  972. }
  973. }
  974. private function textualdate2unix($number){
  975. $number =
  976. explode(
  977. " ",
  978. str_replace(
  979. [
  980. " ago",
  981. "seconds",
  982. "minutes",
  983. "hours",
  984. "days",
  985. "weeks",
  986. "months",
  987. "years"
  988. ],
  989. [
  990. "",
  991. "second",
  992. "minute",
  993. "hour",
  994. "day",
  995. "week",
  996. "month",
  997. "year"
  998. ],
  999. $number
  1000. ),
  1001. 2
  1002. );
  1003. $time = 0;
  1004. switch($number[1]){
  1005. case "second":
  1006. $time = (int)$number[0];
  1007. break;
  1008. case "minute":
  1009. $time = (int)$number[0] * 60;
  1010. break;
  1011. case "hour":
  1012. $time = (int)$number[0] * 3600;
  1013. break;
  1014. case "day":
  1015. $time = (int)$number[0] * 86400;
  1016. break;
  1017. case "week":
  1018. $time = (int)$number[0] * 604800;
  1019. break;
  1020. case "month":
  1021. $time = (int)$number[0] * 2629746;
  1022. break;
  1023. case "year":
  1024. $time = (int)$number[0] * 31556952;
  1025. break;
  1026. }
  1027. return time() - $time;
  1028. }
  1029. private function checkhttpspresence($link){
  1030. if(substr($link, 0, 2) == "//"){
  1031. return "https:" . $link;
  1032. }
  1033. return $link;
  1034. }
  1035. private function textualtime2int($number){
  1036. $number = explode(" - ", $number);
  1037. if(count($number) >= 2){
  1038. $number = $number[count($number) - 2];
  1039. }else{
  1040. $number = $number[0];
  1041. }
  1042. $number =
  1043. str_replace(
  1044. [
  1045. " ",
  1046. "seconds",
  1047. "minutes",
  1048. "hours",
  1049. ],
  1050. [
  1051. "",
  1052. "second",
  1053. "minute",
  1054. "hour"
  1055. ],
  1056. $number
  1057. );
  1058. preg_match_all(
  1059. '/([0-9]+)(second|minute|hour)/',
  1060. $number,
  1061. $number
  1062. );
  1063. $time = 0;
  1064. for($i=0; $i<count($number[0]); $i++){
  1065. switch($number[2][$i]){
  1066. case "second":
  1067. $time = $time + (int)$number[1][$i];
  1068. break;
  1069. case "minute":
  1070. $time = $time + ((int)$number[1][$i] * 60);
  1071. break;
  1072. case "hour":
  1073. $time = $time + ((int)$number[1][$i] * 3600);
  1074. break;
  1075. }
  1076. }
  1077. return $time;
  1078. }
  1079. private function views2int($views){
  1080. return
  1081. (int)str_replace(
  1082. ",", "",
  1083. explode(" ", $views, 2)[0]
  1084. );
  1085. }
  1086. private function hms2int($time){
  1087. $parts = explode(":", $time, 3);
  1088. $time = 0;
  1089. if(count($parts) === 3){
  1090. // hours
  1091. $time = $time + ((int)$parts[0] * 3600);
  1092. array_shift($parts);
  1093. }
  1094. if(count($parts) === 2){
  1095. // minutes
  1096. $time = $time + ((int)$parts[0] * 60);
  1097. array_shift($parts);
  1098. }
  1099. // seconds
  1100. $time = $time + (int)$parts[0];
  1101. return $time;
  1102. }
  1103. private function truncatedcount2int($number){
  1104. // decimal should always be 1 number long
  1105. $number = explode(" ", $number, 2);
  1106. $number = $number[0];
  1107. $unit = strtolower($number[strlen($number) - 1]);
  1108. $tmp = explode(".", $number, 2);
  1109. $number = (int)$number;
  1110. if(count($tmp) === 2){
  1111. $decimal = (int)$tmp[1];
  1112. }else{
  1113. $decimal = 0;
  1114. }
  1115. switch($unit){
  1116. case "k":
  1117. $exponant = 1000;
  1118. break;
  1119. case "m":
  1120. $exponant = 1000000;
  1121. break;
  1122. case "b";
  1123. $exponant = 1000000000;
  1124. break;
  1125. default:
  1126. $exponant = 1;
  1127. break;
  1128. }
  1129. return ($number * $exponant) + ($decimal * ($exponant / 10));
  1130. }
  1131. private function titledots($title){
  1132. $substr = substr($title, -3);
  1133. if(
  1134. $substr == "..." ||
  1135. $substr == "…"
  1136. ){
  1137. return str_replace(["\r", "\n"], " ", trim(substr($title, 0, -3), " \n\r\t\v\x00\0\x0B\xc2\xa0"));
  1138. }
  1139. return str_replace(["\r", "\n"], " ", trim($title, " \n\r\t\v\x00\0\x0B\xc2\xa0"));
  1140. }
  1141. private function parse_date($date){
  1142. if(
  1143. preg_match(
  1144. '/^([0-9]+)(s|m|h|d|w|mo|y) ago$/',
  1145. $date,
  1146. $control_chars
  1147. )
  1148. ){
  1149. switch($control_chars[2]){
  1150. case "s": $selection = "seconds"; break;
  1151. case "m": $selection = "minutes"; break;
  1152. case "h": $selection = "hours"; break;
  1153. case "d": $selection = "days"; break;
  1154. case "w": $selection = "weeks"; break;
  1155. case "mo": $selection = "months"; break;
  1156. case "y": $selection = "years"; break;
  1157. default: return null;
  1158. }
  1159. $date = "{$control_chars[1]} {$selection} ago";
  1160. }
  1161. $out = strtotime($date);
  1162. if($out === false){ $out = null; }
  1163. return $out;
  1164. }
  1165. private function assemble_runs($runs){
  1166. if(!isset($runs["runs"])){ return ""; }
  1167. $text = "";
  1168. foreach($runs["runs"] as $run){
  1169. if(isset($run["text"])){
  1170. $text .= $run["text"];
  1171. }
  1172. }
  1173. return $text;
  1174. }
  1175. }