Oppaitime's version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mediainfo.class.php 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. <?php
  2. class MediaInfo {
  3. public static function parse($string, $raw=false) {
  4. $t = new ParseManager($string);
  5. $t->parse();
  6. return $raw ? $t->output_raw() : $t->output();
  7. }
  8. }
  9. class ParseManager {
  10. protected $lines;
  11. protected $parsed_lines;
  12. protected $index;
  13. protected $parsers;
  14. protected $output;
  15. protected $available_parsers = array(
  16. 'general'=> 'GeneralSectionParser',
  17. 'video'=> 'VideoSectionParser',
  18. 'audio'=> 'AudioSectionParser',
  19. 'text'=> 'TextSectionParser',
  20. );
  21. const GENERIC_PARSER = 'generic_parser';
  22. const MEDIAINFO_START = 'general';
  23. public function __construct($string='') {
  24. $this->index = 0;
  25. $this->output = '';
  26. $this->parsed_lines = [];
  27. $this->set_string($string);
  28. $p = new SectionParser();
  29. $this->add_parser($p);
  30. }
  31. protected function set_string($string) {
  32. $string = trim($string);
  33. $string = static::strip_escaped_tags($string);
  34. $lines = preg_split("/\r\n|\n|\r/", $string);
  35. array_walk($lines,function(&$l) {
  36. $l=trim($l);
  37. });
  38. $this->lines = $lines;
  39. }
  40. protected function add_parser($p,$name='') {
  41. $p->set_lines($this->lines,$this->index);
  42. if (!$name) $name = static::GENERIC_PARSER;
  43. $this->parsers[$name][] = $p;
  44. }
  45. public function parse() {
  46. // get the next section
  47. while ($this->index < count($this->lines) &&
  48. !($line = $this->parsers[static::GENERIC_PARSER][0]->parse_line()));
  49. $section = SectionParser::section_name($line);
  50. $this->index--; // go back to line we just read
  51. // we can have multiple mediainfo files inside the block, so handle that case here
  52. if ($section == self::MEDIAINFO_START && isset($this->parsers[$section])) {
  53. $this->new_mediainfo();
  54. }
  55. if (isset($this->available_parsers[$section])){
  56. $parser = new $this->available_parsers[$section];
  57. $this->add_parser($parser,$section);
  58. // parse section using the parser
  59. while ($line = $parser->parse_line()) $this->parsed_lines[] = $line;
  60. $this->parsed_lines[] = '';
  61. }
  62. else {
  63. // skip until the next blank line or until the next general section
  64. while ($line = $this->parsers[static::GENERIC_PARSER][0]->parse_line()){
  65. $section = SectionParser::section_name($line);
  66. if ($section == self::MEDIAINFO_START) {
  67. $this->index--; // go back to line we just read
  68. break;
  69. }
  70. }
  71. }
  72. // keep iterating until the last line
  73. if ($this->index < count($this->lines)) {
  74. $this->parse();
  75. }
  76. }
  77. public function output($cummulative=true) {
  78. $string = implode("<br />\n",$this->parsed_lines);
  79. if (!isset($this->parsers['general'])) return $string;
  80. $midiv_start = '<div class="spoilerContainer hideContainer">
  81. <input type="button" class="spoilerButton" onclick="BBCode.spoiler(this);" value="Show '.
  82. $this->parsers['general'][0]->filename.
  83. '" /><blockquote class="spoiler hidden">';
  84. $midiv_end = "</blockquote></div>";
  85. $output = '<table class="mediainfo"><tbody><tr><td>';
  86. $output .= $this->parsers['general'][0]->output();
  87. if (isset($this->parsers['video'])){
  88. $output .= '</td><td>';
  89. $output .= $this->parsers['video'][0]->output();
  90. }
  91. if (isset($this->parsers['audio'])){
  92. $output .= '</td><td>';
  93. $output .= '<table><caption>Audio</caption><tbody>';
  94. foreach($this->parsers['audio'] as $index => $ap) {
  95. $output .= sprintf('<tr><td>#%d: &nbsp;</td><td>%s</td></tr>',intval($index+1),$ap->output());
  96. }
  97. $output .= '</tbody></table>';
  98. }
  99. if (isset($this->parsers['text'])){
  100. // subtitles table will be printed below the previous table
  101. $output .= '<br />';
  102. $output .= '<table><caption>Subtitles</caption><tbody>';
  103. foreach($this->parsers['text'] as $index => $tp) {
  104. $output .= sprintf('<tr><td>#%d: &nbsp;</td><td>%s</td></tr>',intval($index+1),$tp->output());
  105. }
  106. $output .= '</tbody></table>';
  107. }
  108. $output .= '</td></tr></tbody></table><br />';
  109. $output = $midiv_start . $string . $midiv_end . $output;
  110. if ($cummulative) {
  111. $output = $this->output . $output;
  112. }
  113. return $output;
  114. }
  115. public function output_raw() {
  116. $output = [];
  117. $sections = ['general', 'video', 'audio', 'text'];
  118. foreach($sections as $section) {
  119. if (isset($this->parsers[$section])) {
  120. $output[$section] = [];
  121. foreach($this->parsers[$section] as $index => $parser) {
  122. $output[$section][$index] = $parser->output_raw();
  123. }
  124. }
  125. }
  126. return $output;
  127. }
  128. // strip escaped html tags
  129. // this is not done for security, just to beautify things (html should already be escaped)
  130. public static function strip_escaped_tags($string) {
  131. // use the php function first
  132. $string = strip_tags($string);
  133. $gt = '&gt;|&#62;|>';
  134. $lt = '&lt;|&#60;|<';
  135. // there is no opening tag, so don't go through the rest of the regexes
  136. if (!preg_match("($lt)",$string))
  137. return $string;
  138. $tag_match = "/(?:$lt)(?P<tag>(?:(?!$gt).)*)(?:$gt)/ims";
  139. // this should match and remove tags
  140. $string = preg_replace($tag_match,'',$string);
  141. return $string;
  142. }
  143. protected function new_mediainfo() {
  144. $this->output .= $this->output(false);
  145. $this->parsed_lines = [];
  146. foreach(array_keys($this->parsers) as $key) {
  147. if ($key != static::GENERIC_PARSER)
  148. unset($this->parsers[$key]);
  149. }
  150. }
  151. }
  152. class SectionParser {
  153. protected $lines;
  154. protected $index;
  155. public function __construct(&$lines=[],&$i=0) {
  156. $this->set_lines($lines,$i);
  157. }
  158. public function set_lines(&$lines=[],&$i=0){
  159. $this->lines = &$lines;
  160. $this->index = &$i;
  161. }
  162. // should always return the read line
  163. public function parse_line(){
  164. if (!isset($this->lines[$this->index])) {
  165. return null;
  166. }
  167. $line = $this->lines[$this->index++];
  168. $pair = static::property_value_pair($line);
  169. $this->handle_cases($pair['property'],$pair['value']);
  170. return $line;
  171. }
  172. public function output() {
  173. }
  174. protected function handle_cases($property, $value) {
  175. }
  176. public static function section_name($string) {
  177. if (!$string) return false;
  178. $mistart="/^(?:general$|unique id|complete name)/i";
  179. if (preg_match($mistart, $string)) return ParseManager::MEDIAINFO_START;
  180. $words = explode(' ',$string);
  181. return strtolower($words[0]);
  182. }
  183. public static function property_value_pair($string) {
  184. $pair = explode(":", $string, 2);
  185. return array('property'=>strtolower(trim($pair[0])),'value'=>trim($pair[1]??''));
  186. }
  187. public static function strip_path($string) { // remove filepath
  188. $string = str_replace("\\", "/", $string);
  189. $path_parts = pathinfo($string);
  190. return $path_parts['basename'];
  191. }
  192. public static function parse_size($string) {
  193. return str_replace(array('pixels', ' '), null, $string);
  194. }
  195. protected static function table_head($caption) {
  196. return "<table class='nobr'><caption>$caption</caption><tbody>";
  197. }
  198. protected static function table_row($property,$value) {
  199. if ($value)
  200. return "<tr><td>$property:&nbsp;&nbsp;</td><td>$value</td></tr>";
  201. return '';
  202. }
  203. protected static function table_tail(){
  204. return '</tbody></table>';
  205. }
  206. }
  207. class AudioSectionParser extends SectionParser {
  208. protected $audioformat;
  209. protected $audiobitrate;
  210. protected $audiochannels;
  211. protected $audiochannelpositions;
  212. protected $audiotitle;
  213. protected $audiolang;
  214. protected $audioprofile;
  215. protected $form_audioformat;
  216. protected $form_audiochannels;
  217. protected function handle_cases($property,$value) {
  218. switch ($property) {
  219. case "format":
  220. $this->audioformat = $value;
  221. break;
  222. case "bit rate":
  223. $this->audiobitrate = $value;
  224. break;
  225. case "channel(s)":
  226. $this->audiochannels = $value;
  227. break;
  228. case "channel positions":
  229. $this->audiochannelpositions = $value;
  230. break;
  231. case "title":
  232. $this->audiotitle = $value;
  233. break;
  234. case "language":
  235. $this->audiolang = $value;
  236. break;
  237. case "format profile":
  238. $this->audioprofile = $value;
  239. break;
  240. }
  241. }
  242. public function output() {
  243. $this->process_vars();
  244. $output = $this->audiolang . ' ' . $this->channels() . ' ' . $this->format();
  245. $output .= ($this->audiobitrate) ? " @ $this->audiobitrate" : '';
  246. $output .= ($this->audiotitle) ? " ($this->audiotitle)" : '';
  247. return $output;
  248. }
  249. public function output_raw() {
  250. $this->process_vars();
  251. $output = [];
  252. $properties = [
  253. 'audioformat', 'audiobitrate', 'audiochannels',
  254. 'audiochannelpositions', 'audiotitle', 'audiolang', 'audioprofile',
  255. 'form_audioformat', 'form_audiochannels'
  256. ];
  257. foreach($properties as $property) {
  258. if ($this->$property) $output[$property] = $this->$property;
  259. }
  260. return $output;
  261. }
  262. protected function process_vars() {
  263. $this->form_audioformat = $this->form_format();
  264. $this->form_audiochannels = $this->form_channels();
  265. }
  266. protected function format() {
  267. if (strtolower($this->audioformat) === 'mpeg audio') {
  268. switch (strtolower($this->audioprofile)) {
  269. case 'layer 3':
  270. return 'MP3';
  271. case 'layer 2':
  272. return 'MP2';
  273. case 'layer 1':
  274. return 'MP1';
  275. }
  276. }
  277. return $this->audioformat;
  278. }
  279. protected function form_format() {
  280. // Not implemented: Real Audio, DTS-HD
  281. switch (strtolower($this->format())) {
  282. case 'mp2':
  283. return 'MP2';
  284. case 'mp3':
  285. return 'MP3';
  286. case 'vorbis':
  287. return 'OGG';
  288. case 'aac':
  289. return 'AAC';
  290. case 'ac-3':
  291. return 'AC3';
  292. case 'truehd':
  293. return 'TrueHD';
  294. case 'dts':
  295. switch (strtolower($this->audioprofile)) {
  296. case 'es':
  297. return 'DTS-ES';
  298. case 'ma / core':
  299. return 'DTS-HD MA';
  300. default:
  301. return 'DTS';
  302. }
  303. case 'flac':
  304. return 'FLAC';
  305. case 'pcm':
  306. return 'PCM';
  307. case 'wma':
  308. return 'WMA';
  309. }
  310. }
  311. protected function channels() {
  312. if (isset($this->audiochannels)) {
  313. $chans = preg_replace('/^(\d).*$/', '$1', $this->audiochannels);
  314. if (isset($this->audiochannelpositions) && preg_match('/LFE/',
  315. $this->audiochannelpositions)) {
  316. $chans -= .9;
  317. } else {
  318. $chans = $chans . '.0';
  319. }
  320. return $chans . 'ch';
  321. }
  322. }
  323. protected function form_channels() {
  324. return preg_replace('/ch/', '', $this->channels());
  325. }
  326. }
  327. class GeneralSectionParser extends SectionParser {
  328. public $filename;
  329. protected $generalformat;
  330. protected $duration;
  331. protected $filesize;
  332. protected $form_codec;
  333. protected $form_releasegroup;
  334. protected function handle_cases($property,$value) {
  335. switch ($property) {
  336. case "complete name":
  337. // Remove autodetected urls
  338. $value = preg_replace('#\[autourl(?:=.+)?\](.+)\[/autourl\]#', '$1', $value);
  339. $this->filename = static::strip_path($value);
  340. $this->lines[$this->index-1] = "Complete name : " . $this->filename;
  341. $this->filename = substr($this->filename, 0, 150);
  342. break;
  343. case "format":
  344. $this->generalformat = $value;
  345. break;
  346. case "duration":
  347. $this->duration = $value;
  348. break;
  349. case "file size":
  350. $this->filesize = $value;
  351. break;
  352. }
  353. }
  354. public function output() {
  355. $this->process_vars();
  356. $output = static::table_head('General');
  357. $properties = [
  358. 'Container' => 'generalformat',
  359. 'Runtime' => 'duration',
  360. 'Size' => 'filesize'
  361. ];
  362. foreach($properties as $property => $value) {
  363. $output .= static::table_row($property,$this->$value);
  364. }
  365. $output .= static::table_tail();
  366. return $output;
  367. }
  368. public function output_raw() {
  369. $this->process_vars();
  370. $output = [];
  371. $properties = [
  372. 'filename', 'generalformat', 'duration', 'filesize', 'form_codec',
  373. 'form_releasegroup'
  374. ];
  375. foreach($properties as $property) {
  376. if ($this->$property) $output[$property] = $this->$property;
  377. }
  378. return $output;
  379. }
  380. protected function process_vars() {
  381. switch (strtolower($this->generalformat)) {
  382. case 'mpeg-ts':
  383. $this->form_codec = 'MPEG-TS';
  384. break;
  385. // We can't determine if it's DVD5 or DVD9, so don't guess.
  386. case 'mpeg-ps':
  387. $this->form_codec = '---';
  388. break;
  389. }
  390. $matches = [];
  391. preg_match('/(?:^|.*\\|\/)\[(.*?)\].*$/',
  392. $this->filename, $matches);
  393. $this->form_releasegroup = $matches ? $matches[1] : '';
  394. }
  395. }
  396. class TextSectionParser extends SectionParser {
  397. protected $title;
  398. protected $language;
  399. protected $format;
  400. protected $default;
  401. protected $processed_language;
  402. protected $form_format;
  403. protected function handle_cases($property,$value) {
  404. switch ($property) {
  405. case 'title':
  406. $this->title = $value;
  407. break;
  408. case 'language':
  409. $this->language = $value;
  410. break;
  411. case 'format':
  412. $this->format = $value;
  413. break;
  414. case 'default':
  415. $this->default = ($value == 'Yes');
  416. break;
  417. }
  418. }
  419. public function output() {
  420. $this->process_vars();
  421. $language = $this->processed_language;
  422. $output = "$language ($this->format)";
  423. if ($this->title) $output .= " ($this->title)";
  424. if ($this->default) $output .= ' (default)';
  425. return $output;
  426. }
  427. public function output_raw() {
  428. $this->process_vars();
  429. $output = [];
  430. $properties = [
  431. 'title', 'language', 'format', 'default', 'processed_language',
  432. 'form_format'
  433. ];
  434. foreach($properties as $property) {
  435. if ($this->$property) $output[$property] = $this->$property;
  436. }
  437. return $output;
  438. }
  439. protected function process_vars() {
  440. $this->processed_language = ($this->language) ?
  441. $this->language : 'Unknown';
  442. $this->form_format = 'Softsubbed';
  443. }
  444. }
  445. class VideoSectionParser extends SectionParser {
  446. protected $videoformat;
  447. protected $videoformatversion;
  448. protected $codec;
  449. protected $width;
  450. protected $height;
  451. protected $writinglibrary;
  452. protected $frameratemode;
  453. protected $framerate;
  454. protected $aspectratio;
  455. protected $bitrate;
  456. protected $bitratemode;
  457. protected $nominalbitrate;
  458. protected $bpp;
  459. protected $bitdepth;
  460. protected $processed_codec;
  461. protected $processed_resolution;
  462. protected $processed_framerate;
  463. protected $form_codec;
  464. protected $form_resolution;
  465. protected function handle_cases($property,$value) {
  466. switch ($property) {
  467. case "format":
  468. $this->videoformat = $value;
  469. break;
  470. case "format version":
  471. $this->videoformatversion = $value;
  472. break;
  473. case "codec id":
  474. $this->codec = strtolower($value);
  475. break;
  476. case "width":
  477. $this->width = static::parse_size($value);
  478. break;
  479. case "height":
  480. $this->height = static::parse_size($value);
  481. break;
  482. case "writing library":
  483. $this->writinglibrary = $value;
  484. break;
  485. case "frame rate mode":
  486. $this->frameratemode = $value;
  487. break;
  488. case "frame rate":
  489. // if variable this becomes Original frame rate
  490. $this->framerate = $value;
  491. break;
  492. case "display aspect ratio":
  493. $this->aspectratio = $value;
  494. break;
  495. case "bit rate":
  496. $this->bitrate = $value;
  497. break;
  498. case "bit rate mode":
  499. $this->bitratemode = $value;
  500. break;
  501. case "nominal bit rate":
  502. $this->nominalbitrate = $value;
  503. break;
  504. case "bits/(pixel*frame)":
  505. $this->bpp = $value;
  506. break;
  507. case 'bit depth':
  508. $this->bitdepth = $value;
  509. break;
  510. }
  511. }
  512. public function output() {
  513. $this->process_vars();
  514. $output = static::table_head('Video');
  515. $properties = [
  516. 'Codec' => 'processed_codec',
  517. 'Bit depth' => 'bitdepth',
  518. 'Resolution' => 'processed_resolution',
  519. 'Aspect ratio' => 'aspectratio',
  520. 'Frame rate' => 'processed_framerate',
  521. 'Bit rate' => 'bitrate',
  522. 'BPP' => 'bpp'
  523. ];
  524. foreach($properties as $property => $value) {
  525. $output .= static::table_row($property,$this->$value);
  526. }
  527. $output .= static::table_tail();
  528. return $output;
  529. }
  530. public function output_raw() {
  531. $this->process_vars();
  532. $output = [];
  533. $properties = [
  534. 'videoformat', 'videoformatversion', 'codec', 'width', 'height',
  535. 'writinglibrary', 'frameratemode', 'framerate', 'aspectratio',
  536. 'bitrate', 'bitratemode', 'nominalbitrate', 'bpp', 'bitdepth',
  537. 'processed_codec', 'processed_resolution', 'processed_framerate',
  538. 'form_codec', 'form_resolution'
  539. ];
  540. foreach($properties as $property) {
  541. if ($this->$property) $output[$property] = $this->$property;
  542. }
  543. return $output;
  544. }
  545. protected function process_vars() {
  546. $this->processed_codec = $this->compute_codec();
  547. $this->processed_resolution = ($this->width) ?
  548. $this->width . 'x' . $this->height : '';
  549. $this->processed_framerate = (strtolower($this->frameratemode) !=
  550. "constant" && $this->frameratemode) ?
  551. $this->frameratemode : $this->framerate;
  552. $this->form_codec = $this->compute_form_codec();
  553. $this->form_resolution = $this->compute_form_resolution();
  554. }
  555. protected function compute_codec() {
  556. switch (strtolower($this->videoformat)) {
  557. case "mpeg video":
  558. switch (strtolower($this->videoformatversion)) {
  559. case "version 2":
  560. return "MPEG-2";
  561. case "version 1":
  562. return "MPEG-1";
  563. }
  564. return $this->videoformat;
  565. }
  566. switch (strtolower($this->codec)) {
  567. case "div3":
  568. return "DivX 3";
  569. case "divx":
  570. case "dx50":
  571. return "DivX";
  572. case "xvid":
  573. return "XviD";
  574. case "x264":
  575. return "x264";
  576. }
  577. $chk = strtolower($this->codec);
  578. $wl = strtolower($this->writinglibrary);
  579. if (($chk === "v_mpeg4/iso/avc" || $chk === "avc1") && strpos($wl, "x264 core") === FALSE) {
  580. return "H264";
  581. } else if (($chk === "v_mpeg4/iso/avc" || $chk === "avc1") && strpos($wl, "x264 core") > -1) {
  582. return "x264";
  583. } else if (strtolower($this->videoformat) === "avc" && strpos($wl, "x264 core") === FALSE) {
  584. return "H264";
  585. }
  586. if (($chk === 'v_mpegh/iso/hevc') || ($wl === 'hevc'))
  587. return 'H265';
  588. }
  589. protected function compute_form_codec() {
  590. // Not implemented: DVD5, DVD9, WMV, Real Video
  591. // MPEG-TS set as GeneralSectionParser::$form_codec if found
  592. // MPEG-PS sets GeneralSectionParser::$form_codec to blank form value
  593. // so DVD5 or DVD9 is selected manually.
  594. $codec = $this->compute_codec();
  595. switch(strtolower($codec)) {
  596. case 'x264':
  597. case 'h264':
  598. return strtolower($this->bitdepth) == '10 bits' ?
  599. 'h264 10-bit' : 'h264';
  600. case 'h265':
  601. return 'h265';
  602. case 'xvid':
  603. return 'XviD';
  604. case 'divx':
  605. case 'divx 3':
  606. return 'DivX';
  607. case 'mpeg-1':
  608. return 'MPEG';
  609. case 'mpeg-2':
  610. return 'MPEG-2';
  611. }
  612. switch(strtolower($this->codec)) {
  613. case 'wmv3':
  614. return 'VC-1';
  615. case 'mp43':
  616. return 'MPEG-4 v3';
  617. }
  618. switch(strtolower($this->videoformat)) {
  619. case 'vc-1':
  620. return 'VC-1';
  621. case 's-mpeg 4 v3':
  622. return 'MPEG-4 v3';
  623. }
  624. }
  625. protected function compute_form_resolution() {
  626. global $Resolutions;
  627. $closest = null;
  628. if (isset($this->height)) {
  629. $resolutions = $Resolutions;
  630. foreach($resolutions as $resolution) {
  631. if (!isset($closest) || abs($this->height - $resolution) <
  632. abs($this->height - $closest)) {
  633. $closest = $resolution;
  634. }
  635. }
  636. }
  637. return $closest;
  638. }
  639. }