ytclass.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. <?php
  2. class YTDownloader {
  3. private $cache_dir;
  4. private $cookie_dir;
  5. private $appSettings;
  6. private $itag_info = array(
  7. // Full Video
  8. 5 => "FLV[400x240]",
  9. 6 => "FLV[450x270]",
  10. 17 => "3GP[176x144]",
  11. 18 => "MP4[640x360]",
  12. 22 => "HD MP4[1280x720]",
  13. 34 => "FLV[640x360]",
  14. 35 => "FLV[854x480]",
  15. 36 => "3GP[320x180]",
  16. 37 => "MP4[1920x1080]",
  17. 38 => "MP4[4096x3072]",
  18. 43 => "WEBM[640x360]",
  19. 44 => "WEBM[854x480]",
  20. 45 => "WEBM[1280x720]",
  21. 46 => "WEBM[1920x1080]",
  22. 59 => "MP4[854x480]",
  23. 78 => "MP4[854x480]",
  24. // DASH videos
  25. 137 => "(Video Only) MP4[1920x1080]",
  26. 248 => "(Video Only) WEBM[1920x1080]",
  27. 136 => "(Video Only) MP4[1280x720]",
  28. 247 => "(Video Only) WEBM[1280x720]",
  29. 135 => "(Video Only) MP4[854x480]",
  30. 244 => "(Video Only) WEBM[854x480]",
  31. 134 => "(Video Only) MP4[640x360]",
  32. 243 => "(Video Only) WEBM[640x360]",
  33. 133 => "(Video Only) MP4[320x240]",
  34. 242 => "(Video Only) WEBM[320x240]",
  35. 160 => "(Video Only) MP4[176x144]",
  36. 278 => "(Video Only) WEBM[176x144]",
  37. // Dash Audios
  38. 140 => "(Audio Only) M4A[128Kbps]",
  39. 171 => "(Audio Only) WEBM[128Kbps]",
  40. 249 => "(Audio Only) WEBM[50Kbps]",
  41. 250 => "(Audio Only) WEBM[70Kbps]",
  42. 251 => "(Audio Only) WEBM[160Kbps]"
  43. );
  44. private $itag_ext = array(
  45. // Full Video
  46. 5 => ".flv",
  47. 6 => ".flv",
  48. 17 => ".3gp",
  49. 18 => ".mp4",
  50. 22 => ".mp4",
  51. 34 => ".flv",
  52. 35 => ".flv",
  53. 36 => ".3gp",
  54. 37 => ".mp4",
  55. 38 => ".mp4",
  56. 43 => ".webm",
  57. 44 => ".webm",
  58. 45 => ".webm",
  59. 46 => ".webm",
  60. 59 => ".mp4",
  61. 78 => ".mp4",
  62. // DASH videos
  63. 137 => ".mp4",
  64. 248 => ".webm",
  65. 136 => ".mp4",
  66. 247 => ".webm",
  67. 135 => ".mp4",
  68. 244 => ".webm",
  69. 134 => ".mp4",
  70. 243 => ".webm",
  71. 133 => ".mp4",
  72. 242 => ".webm",
  73. 160 => ".mp4",
  74. 278 => ".webm",
  75. // Dash Audios
  76. 140 => ".mp4",
  77. 171 => ".webm",
  78. 249 => ".webm",
  79. 250 => ".webm",
  80. 251 => ".webm"
  81. );
  82. function __construct(){
  83. $this->cache_dir = dirname(__FILE__).'/.cache';
  84. $this->cookie_dir = sys_get_temp_dir();
  85. $this->appSettings = parse_ini_file('../config/config.ini',true);
  86. if(!file_exists($this->cache_dir) && is_writeable(dirname(__FILE__))) {
  87. mkdir($this->cache_dir,0755);
  88. }
  89. }
  90. public function getDownloadLinks($id) {
  91. $returnData = FALSE;
  92. $videoID = $this->extractId($id);
  93. $webPage = $this->curlGet('https://www.youtube.com/watch?v='.$videoID);
  94. if($webPage) {
  95. $sts = null;
  96. if(preg_match('|"sts":([0-9]{4,}),"|i', $webPage, $matches)) {
  97. $sts = $matches[1];
  98. }
  99. foreach(array('vevo', 'embedded', 'detailpage') as $elKey) {
  100. $query = http_build_query(array(
  101. 'c' => 'web',
  102. 'el' => $elKey,
  103. 'hl' => 'en_US',
  104. 'sts' => $sts,
  105. 'cver' => 'html5',
  106. 'eurl' => "https://youtube.googleapis.com/v/{$videoID}",
  107. 'html5' => '1',
  108. 'iframe' => '1',
  109. 'authuser' => '1',
  110. 'video_id' => $videoID,
  111. ));
  112. if($this->is_Ok($videoData = $this->curlGet("https://www.youtube.com/get_video_info?{$query}"))) {
  113. parse_str($videoData, $videoData);
  114. break;
  115. }
  116. }
  117. if(isset($videoData['status']) && $videoData['status'] !== 'fail') {
  118. $playerData = json_decode($videoData["player_response"]);
  119. $captions = array();
  120. for($i=0;$i<count($playerData->captions->playerCaptionsTracklistRenderer->captionTracks);$i++) {
  121. $caption = array();
  122. $caption["title"] = $playerData->captions->playerCaptionsTracklistRenderer->captionTracks[$i]->name->simpleText;
  123. $caption["lang"] = $playerData->captions->playerCaptionsTracklistRenderer->captionTracks[$i]->languageCode;
  124. $caption["url"] = $playerData->captions->playerCaptionsTracklistRenderer->captionTracks[$i]->baseUrl;
  125. array_push($captions,$caption);
  126. }
  127. $thumbinfo = $playerData->storyboards->playerStoryboardSpecRenderer->spec;
  128. $thumbparts = explode("|",$thumbinfo);
  129. $thumbnum = count($thumbparts)-1;
  130. $thumbdata = explode("#",$thumbparts[$thumbnum]);
  131. $vInfo['Title'] = $playerData->videoDetails->title;
  132. $vInfo['ChannelName'] = $playerData->videoDetails->author;
  133. $vInfo['ChannelId'] = $playerData->videoDetails->channelId;
  134. $vInfo['Thumbnail'] = $playerData->videoDetails->thumbnail->thumbnails[count($playerData->videoDetails->thumbnail->thumbnails)-1]->url;
  135. $vInfo['Duration'] = $playerData->videoDetails->lengthSeconds;
  136. $vInfo['Rating'] = $playerData->videoDetails->averageRating;
  137. $vInfo['Captions'] = $captions;
  138. $vInfo['Thumbs'] = array();
  139. $vInfo['Thumbs']["src"] = "ytthumbs.php?data=".urlencode($thumbinfo);
  140. $vInfo['Thumbs']["width"] = $thumbdata[0]*$thumbdata[3];
  141. $vInfo['Thumbs']["height"] = $thumbdata[1]*ceil($thumbdata[2]/$thumbdata[3]);
  142. $vInfo['Thumbs']["fwidth"] = $thumbdata[0];
  143. $vInfo['Thumbs']["fheight"] = $thumbdata[1];
  144. $vInfo['Thumbs']["fcount"] = $thumbdata[2];
  145. $vInfo['Thumbs']["row"] = $thumbdata[3];
  146. }
  147. if (isset($playerData->streamingData->formats) && isset($playerData->streamingData->adaptiveFormats)) {
  148. $draft1 = $playerData->streamingData->formats;
  149. $draft2 = $playerData->streamingData->adaptiveFormats;
  150. foreach ($draft1 as $key) {
  151. $draftLink[] = $key;
  152. }
  153. foreach ($draft2 as $key) {
  154. $draftLink[] = $key;
  155. }
  156. foreach($draftLink as $dlink) {
  157. if(isset($dlink->cipher)) parse_str($dlink->cipher,$mLink[]);
  158. else $mLink[] = array("url"=>$dlink->url);
  159. $mLink[count($mLink)-1]["itag"] = $dlink->itag;
  160. }
  161. if (isset($mLink[0]['s'])) {
  162. $instructions = $this->get_instructions($webPage);
  163. }
  164. foreach($mLink as $linker) {
  165. if(isset($linker['s'])) {
  166. $linkData[] = array(
  167. 'url' => preg_replace('@(https\:\/\/)[^\.]+(\.googlevideo\.com)@', 'https://redirector$2', $linker['url']).'&'.$linker["sp"].'='.$this->sig_decode($linker['s'], $instructions).'&title='.$this->clean_name($playerData->videoDetails->title),
  168. 'itag' => $linker['itag'],
  169. 'type' => isset($this->itag_info[$linker['itag']]) ? $this->itag_info[$linker['itag']] : 'Unknown'
  170. );
  171. } else {
  172. $linkData[] = array(
  173. 'url' => preg_replace('@(https\:\/\/)[^\.]+(\.googlevideo\.com)@', 'https://redirector$2', $linker['url']).'&title='.$this->clean_name($playerData->videoDetails->title),
  174. 'itag' => $linker['itag'],
  175. 'type' => isset($this->itag_info[$linker['itag']]) ? $this->itag_info[$linker['itag']] : 'Unknown'
  176. );
  177. }
  178. }
  179. }
  180. }
  181. if(!empty($vInfo)) $returnData['info'] = $vInfo;
  182. if(!empty($linkData)) $returnData['dl'] = $linkData;
  183. if(!$returnData && $this->appSettings["Media"]["youplay_fallback"]) $returnData = $this->appSettings["Media"]["invidious"];
  184. return $returnData;
  185. }
  186. protected function curlGet($url) {
  187. if(in_array('curl', get_loaded_extensions())){
  188. $ch = curl_init($url);
  189. curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0');
  190. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  191. curl_setopt($ch, CURLOPT_HEADER, 0);
  192. //curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  193. if($this->appSettings["Proxy"]["type"]) {
  194. curl_setopt($ch, CURLOPT_PROXY, $this->appSettings["Proxy"]["type"]."://".$this->appSettings["Proxy"]["domain"].":".$this->appSettings["Proxy"]["port"]);
  195. curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->appSettings["Proxy"]["username"].":".$this->appSettings["Proxy"]["password"]);
  196. }
  197. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
  198. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
  199. $result = curl_exec($ch);
  200. curl_close($ch);
  201. return $result;
  202. }
  203. return FALSE;
  204. }
  205. private function is_Ok($var) {
  206. if(!preg_match('|status=fail|i',$var)) {
  207. return true;
  208. }
  209. }
  210. private function extractId($str) {
  211. if(preg_match('/[a-z0-9_-]{11}/i', $str, $matches)){
  212. return $matches[0];
  213. }
  214. return FALSE;
  215. }
  216. private function get_instructions($html) {
  217. $playerPattern = '/"assets":.+?"js":\s*("[^"]+")/';
  218. if(preg_match($playerPattern, $html, $matches) && is_string($_player = json_decode($matches[1])) && strlen($_player) >= 1) {
  219. $playerLink = substr($_player, 0, 2) == '//' ? "https:{$_player}" : "https://www.youtube.com{$_player}";
  220. $cache_player = $this->cache_dir.'/.ht-'.md5($_player);
  221. if(file_exists($cache_player)) {
  222. return unserialize(file_get_contents($cache_player));
  223. } else {
  224. $js_code = $this->curlGet($playerLink);
  225. if($js_code){
  226. if(file_exists($this->cache_dir) && is_writeable($this->cache_dir))
  227. file_put_contents($cache_player, serialize($js_code));
  228. return $js_code;
  229. }
  230. }
  231. }
  232. return false;
  233. }
  234. private function clean_name($name) {
  235. $special_chars = array(".","?", "[", "]", "/", "\\", "=", "<", ">", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}", "%", "+", chr(0));
  236. $filename = str_replace($special_chars,' ',$name);
  237. $filename = preg_replace( "#\x{00a0}#siu", ' ', $filename );
  238. $filename = str_replace( array( '%20', '+', ' '), '-', $filename );
  239. $filename = preg_replace( '/[\r\n\t -]+/', '-', $filename );
  240. $filename = trim( $filename, '.-_' );
  241. return $filename;
  242. }
  243. private function sig_decode($signature, $js_code) {
  244. $func_name = $this->parseFunctionName($js_code);
  245. $instructions = (array)$this->parseFunctionCode($func_name, $js_code);
  246. foreach($instructions as $opt) {
  247. $command = $opt[0];
  248. $value = $opt[1];
  249. if($command == 'swap') {
  250. $temp = $signature[0];
  251. $signature[0] = $signature[$value % strlen($signature)];
  252. $signature[$value] = $temp;
  253. } elseif ($command == 'splice') {
  254. $signature = substr($signature, $value);
  255. } elseif ($command == 'reverse') {
  256. $signature = strrev($signature);
  257. }
  258. }
  259. return trim($signature);
  260. }
  261. private function parseFunctionName($js_code) {
  262. if (preg_match('@,\s*encodeURIComponent\((\w{2})@is', $js_code, $matches)) {
  263. $func_name = $matches[1];
  264. $func_name = preg_quote($func_name);
  265. return $func_name;
  266. }
  267. else if (preg_match('@\b([a-zA-Z0-9$]{2})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)@is', $js_code, $matches)) {
  268. return preg_quote($matches[1]);
  269. }
  270. return null;
  271. }
  272. private function parseFunctionCode($func_name, $player_htmlz) {
  273. if (preg_match('/' . $func_name . '=function\([a-z]+\){(.*?)}/', $player_htmlz, $matches)) {
  274. $js_code = $matches[1];
  275. if (preg_match_all('/([a-z0-9]{2})\.([a-z0-9]{2})\([^,]+,(\d+)\)/i', $js_code, $matches) != false) {
  276. $obj_list = $matches[1];
  277. $func_list = $matches[2];
  278. preg_match_all('/(' . implode('|', $func_list) . '):function(.*?)\}/m', $player_htmlz, $matches2, PREG_SET_ORDER);
  279. $functions = array();
  280. foreach ($matches2 as $m) {
  281. if (strpos($m[2], 'splice') !== false) {
  282. $functions[$m[1]] = 'splice';
  283. } elseif (strpos($m[2], 'a.length') !== false) {
  284. $functions[$m[1]] = 'swap';
  285. } elseif (strpos($m[2], 'reverse') !== false) {
  286. $functions[$m[1]] = 'reverse';
  287. }
  288. }
  289. $instructions = array();
  290. foreach ($matches[2] as $index => $name) {
  291. $instructions[] = array($functions[$name], $matches[3][$index]);
  292. }
  293. return $instructions;
  294. }
  295. }
  296. return null;
  297. }
  298. }