parse(); return $raw ? $t->output_raw() : $t->output(); } } class ParseManager { protected $lines; protected $parsed_lines; protected $index; protected $parsers; protected $output; protected $available_parsers = array( 'general'=> 'GeneralSectionParser', 'video'=> 'VideoSectionParser', 'audio'=> 'AudioSectionParser', 'text'=> 'TextSectionParser', ); const GENERIC_PARSER = 'generic_parser'; const MEDIAINFO_START = 'general'; public function __construct($string='') { $this->index = 0; $this->output = ''; $this->parsed_lines = array(); $this->set_string($string); $p = new SectionParser(); $this->add_parser($p); } protected function set_string($string) { $string = trim($string); $string = static::strip_escaped_tags($string); $lines = preg_split("/\r\n|\n|\r/", $string); array_walk($lines,function(&$l) { $l=trim($l); }); $this->lines = $lines; } protected function add_parser($p,$name='') { $p->set_lines($this->lines,$this->index); if (!$name) $name = static::GENERIC_PARSER; $this->parsers[$name][] = $p; } public function parse() { // get the next section while ($this->index < count($this->lines) && !($line = $this->parsers[static::GENERIC_PARSER][0]->parse_line())); $section = SectionParser::section_name($line); $this->index--; // go back to line we just read // we can have multiple mediainfo files inside the block, so handle that case here if ($section == self::MEDIAINFO_START && isset($this->parsers[$section])) { $this->new_mediainfo(); } if (isset($this->available_parsers[$section])){ $parser = new $this->available_parsers[$section]; $this->add_parser($parser,$section); // parse section using the parser while ($line = $parser->parse_line()) $this->parsed_lines[] = $line; $this->parsed_lines[] = ''; } else { // skip until the next blank line or until the next general section while ($line = $this->parsers[static::GENERIC_PARSER][0]->parse_line()){ $section = SectionParser::section_name($line); if ($section == self::MEDIAINFO_START) { $this->index--; // go back to line we just read break; } } } // keep iterating until the last line if ($this->index < count($this->lines)) { $this->parse(); } } public function output($cummulative=true) { $string = implode("
\n",$this->parsed_lines); if (!isset($this->parsers['general'])) return $string; $midiv_start = '
"; $output = '
'; $output .= $this->parsers['general'][0]->output(); if (isset($this->parsers['video'])){ $output .= ''; $output .= $this->parsers['video'][0]->output(); } if (isset($this->parsers['audio'])){ $output .= ''; $output .= ''; foreach($this->parsers['audio'] as $index => $ap) { $output .= sprintf('',intval($index+1),$ap->output()); } $output .= '
Audio
#%d:  %s
'; } if (isset($this->parsers['text'])){ // subtitles table will be printed below the previous table $output .= '
'; $output .= ''; foreach($this->parsers['text'] as $index => $tp) { $output .= sprintf('',intval($index+1),$tp->output()); } $output .= '
Subtitles
#%d:  %s
'; } $output .= '

'; $output = $midiv_start . $string . $midiv_end . $output; if ($cummulative) { $output = $this->output . $output; } return $output; } public function output_raw() { $output = array(); $sections = ['general', 'video', 'audio', 'text']; foreach($sections as $section) { if (isset($this->parsers[$section])) { $output[$section] = array(); foreach($this->parsers[$section] as $index => $parser) { $output[$section][$index] = $parser->output_raw(); } } } return $output; } // strip escaped html tags // this is not done for security, just to beautify things (html should already be escaped) public static function strip_escaped_tags($string) { // use the php function first $string = strip_tags($string); $gt = '>|>|>'; $lt = '<|<|<'; // there is no opening tag, so don't go through the rest of the regexes if (!preg_match("($lt)",$string)) return $string; $tag_match = "/(?:$lt)(?P(?:(?!$gt).)*)(?:$gt)/ims"; // this should match and remove tags $string = preg_replace($tag_match,'',$string); return $string; } protected function new_mediainfo() { $this->output .= $this->output(false); $this->parsed_lines = array(); foreach(array_keys($this->parsers) as $key) { if ($key != static::GENERIC_PARSER) unset($this->parsers[$key]); } } } class SectionParser { protected $lines; protected $index; public function __construct(&$lines=array(),&$i=0) { $this->set_lines($lines,$i); } public function set_lines(&$lines=array(),&$i=0){ $this->lines = &$lines; $this->index = &$i; } // should always return the read line public function parse_line(){ if (!isset($this->lines[$this->index])) { return null; } $line = $this->lines[$this->index++]; $pair = static::property_value_pair($line); $this->handle_cases($pair['property'],$pair['value']); return $line; } public function output() { } protected function handle_cases($property, $value) { } public static function section_name($string) { if (!$string) return false; $mistart="/^(?:general$|unique id|complete name)/i"; if (preg_match($mistart, $string)) return ParseManager::MEDIAINFO_START; $words = explode(' ',$string); return strtolower($words[0]); } public static function property_value_pair($string) { $pair = explode(":", $string, 2); return array('property'=>strtolower(trim($pair[0])),'value'=>trim($pair[1]??'')); } public static function strip_path($string) { // remove filepath $string = str_replace("\\", "/", $string); $path_parts = pathinfo($string); return $path_parts['basename']; } public static function parse_size($string) { return str_replace(array('pixels', ' '), null, $string); } protected static function table_head($caption) { return ""; } protected static function table_row($property,$value) { if ($value) return ""; return ''; } protected static function table_tail(){ return '
$caption
$property:  $value
'; } } class AudioSectionParser extends SectionParser { protected $audioformat; protected $audiobitrate; protected $audiochannels; protected $audiochannelpositions; protected $audiotitle; protected $audiolang; protected $audioprofile; protected $form_audioformat; protected $form_audiochannels; protected function handle_cases($property,$value) { switch ($property) { case "format": $this->audioformat = $value; break; case "bit rate": $this->audiobitrate = $value; break; case "channel(s)": $this->audiochannels = $value; break; case "channel positions": $this->audiochannelpositions = $value; break; case "title": $this->audiotitle = $value; break; case "language": $this->audiolang = $value; break; case "format profile": $this->audioprofile = $value; break; } } public function output() { $this->process_vars(); $output = $this->audiolang . ' ' . $this->channels() . ' ' . $this->format(); $output .= ($this->audiobitrate) ? " @ $this->audiobitrate" : ''; $output .= ($this->audiotitle) ? " ($this->audiotitle)" : ''; return $output; } public function output_raw() { $this->process_vars(); $output = array(); $properties = [ 'audioformat', 'audiobitrate', 'audiochannels', 'audiochannelpositions', 'audiotitle', 'audiolang', 'audioprofile', 'form_audioformat', 'form_audiochannels' ]; foreach($properties as $property) { if ($this->$property) $output[$property] = $this->$property; } return $output; } protected function process_vars() { $this->form_audioformat = $this->form_format(); $this->form_audiochannels = $this->form_channels(); } protected function format() { if (strtolower($this->audioformat) === 'mpeg audio') { switch (strtolower($this->audioprofile)) { case 'layer 3': return 'MP3'; case 'layer 2': return 'MP2'; case 'layer 1': return 'MP1'; } } return $this->audioformat; } protected function form_format() { // Not implemented: Real Audio, DTS-HD switch (strtolower($this->format())) { case 'mp2': return 'MP2'; case 'mp3': return 'MP3'; case 'vorbis': return 'OGG'; case 'aac': return 'AAC'; case 'ac-3': return 'AC3'; case 'truehd': return 'TrueHD'; case 'dts': switch (strtolower($this->audioprofile)) { case 'es': return 'DTS-ES'; case 'ma / core': return 'DTS-HD MA'; default: return 'DTS'; } case 'flac': return 'FLAC'; case 'pcm': return 'PCM'; case 'wma': return 'WMA'; } } protected function channels() { if (isset($this->audiochannels)) { $chans = preg_replace('/^(\d).*$/', '$1', $this->audiochannels); if (isset($this->audiochannelpositions) && preg_match('/LFE/', $this->audiochannelpositions)) { $chans -= .9; } else { $chans = $chans . '.0'; } return $chans . 'ch'; } } protected function form_channels() { return preg_replace('/ch/', '', $this->channels()); } } class GeneralSectionParser extends SectionParser { public $filename; protected $generalformat; protected $duration; protected $filesize; protected $form_codec; protected $form_releasegroup; protected function handle_cases($property,$value) { switch ($property) { case "complete name": // Remove autodetected urls $value = preg_replace('#\[autourl(?:=.+)?\](.+)\[/autourl\]#', '$1', $value); $this->filename = static::strip_path($value); $this->lines[$this->index-1] = "Complete name : " . $this->filename; $this->filename = substr($this->filename, 0, 150); break; case "format": $this->generalformat = $value; break; case "duration": $this->duration = $value; break; case "file size": $this->filesize = $value; break; } } public function output() { $this->process_vars(); $output = static::table_head('General'); $properties = [ 'Container' => 'generalformat', 'Runtime' => 'duration', 'Size' => 'filesize' ]; foreach($properties as $property => $value) { $output .= static::table_row($property,$this->$value); } $output .= static::table_tail(); return $output; } public function output_raw() { $this->process_vars(); $output = array(); $properties = [ 'filename', 'generalformat', 'duration', 'filesize', 'form_codec', 'form_releasegroup' ]; foreach($properties as $property) { if ($this->$property) $output[$property] = $this->$property; } return $output; } protected function process_vars() { switch (strtolower($this->generalformat)) { case 'mpeg-ts': $this->form_codec = 'MPEG-TS'; break; // We can't determine if it's DVD5 or DVD9, so don't guess. case 'mpeg-ps': $this->form_codec = '---'; break; } $matches = array(); preg_match('/(?:^|.*\\|\/)\[(.*?)\].*$/', $this->filename, $matches); $this->form_releasegroup = $matches ? $matches[1] : ''; } } class TextSectionParser extends SectionParser { protected $title; protected $language; protected $format; protected $default; protected $processed_language; protected $form_format; protected function handle_cases($property,$value) { switch ($property) { case 'title': $this->title = $value; break; case 'language': $this->language = $value; break; case 'format': $this->format = $value; break; case 'default': $this->default = ($value == 'Yes'); break; } } public function output() { $this->process_vars(); $language = $this->processed_language; $output = "$language ($this->format)"; if ($this->title) $output .= " ($this->title)"; if ($this->default) $output .= ' (default)'; return $output; } public function output_raw() { $this->process_vars(); $output = array(); $properties = [ 'title', 'language', 'format', 'default', 'processed_language', 'form_format' ]; foreach($properties as $property) { if ($this->$property) $output[$property] = $this->$property; } return $output; } protected function process_vars() { $this->processed_language = ($this->language) ? $this->language : 'Unknown'; $this->form_format = 'Softsubbed'; } } class VideoSectionParser extends SectionParser { protected $videoformat; protected $videoformatversion; protected $codec; protected $width; protected $height; protected $writinglibrary; protected $frameratemode; protected $framerate; protected $aspectratio; protected $bitrate; protected $bitratemode; protected $nominalbitrate; protected $bpp; protected $bitdepth; protected $processed_codec; protected $processed_resolution; protected $processed_framerate; protected $form_codec; protected $form_resolution; protected function handle_cases($property,$value) { switch ($property) { case "format": $this->videoformat = $value; break; case "format version": $this->videoformatversion = $value; break; case "codec id": $this->codec = strtolower($value); break; case "width": $this->width = static::parse_size($value); break; case "height": $this->height = static::parse_size($value); break; case "writing library": $this->writinglibrary = $value; break; case "frame rate mode": $this->frameratemode = $value; break; case "frame rate": // if variable this becomes Original frame rate $this->framerate = $value; break; case "display aspect ratio": $this->aspectratio = $value; break; case "bit rate": $this->bitrate = $value; break; case "bit rate mode": $this->bitratemode = $value; break; case "nominal bit rate": $this->nominalbitrate = $value; break; case "bits/(pixel*frame)": $this->bpp = $value; break; case 'bit depth': $this->bitdepth = $value; break; } } public function output() { $this->process_vars(); $output = static::table_head('Video'); $properties = [ 'Codec' => 'processed_codec', 'Bit depth' => 'bitdepth', 'Resolution' => 'processed_resolution', 'Aspect ratio' => 'aspectratio', 'Frame rate' => 'processed_framerate', 'Bit rate' => 'bitrate', 'BPP' => 'bpp' ]; foreach($properties as $property => $value) { $output .= static::table_row($property,$this->$value); } $output .= static::table_tail(); return $output; } public function output_raw() { $this->process_vars(); $output = array(); $properties = [ 'videoformat', 'videoformatversion', 'codec', 'width', 'height', 'writinglibrary', 'frameratemode', 'framerate', 'aspectratio', 'bitrate', 'bitratemode', 'nominalbitrate', 'bpp', 'bitdepth', 'processed_codec', 'processed_resolution', 'processed_framerate', 'form_codec', 'form_resolution' ]; foreach($properties as $property) { if ($this->$property) $output[$property] = $this->$property; } return $output; } protected function process_vars() { $this->processed_codec = $this->compute_codec(); $this->processed_resolution = ($this->width) ? $this->width . 'x' . $this->height : ''; $this->processed_framerate = (strtolower($this->frameratemode) != "constant" && $this->frameratemode) ? $this->frameratemode : $this->framerate; $this->form_codec = $this->compute_form_codec(); $this->form_resolution = $this->compute_form_resolution(); } protected function compute_codec() { switch (strtolower($this->videoformat)) { case "mpeg video": switch (strtolower($this->videoformatversion)) { case "version 2": return "MPEG-2"; case "version 1": return "MPEG-1"; } return $this->videoformat; } switch (strtolower($this->codec)) { case "div3": return "DivX 3"; case "divx": case "dx50": return "DivX"; case "xvid": return "XviD"; case "x264": return "x264"; } $chk = strtolower($this->codec); $wl = strtolower($this->writinglibrary); if (($chk === "v_mpeg4/iso/avc" || $chk === "avc1") && strpos($wl, "x264 core") === FALSE) { return "H264"; } else if (($chk === "v_mpeg4/iso/avc" || $chk === "avc1") && strpos($wl, "x264 core") > -1) { return "x264"; } else if (strtolower($this->videoformat) === "avc" && strpos($wl, "x264 core") === FALSE) { return "H264"; } if (($chk === 'v_mpegh/iso/hevc') || ($wl === 'hevc')) return 'H265'; } protected function compute_form_codec() { // Not implemented: DVD5, DVD9, WMV, Real Video // MPEG-TS set as GeneralSectionParser::$form_codec if found // MPEG-PS sets GeneralSectionParser::$form_codec to blank form value // so DVD5 or DVD9 is selected manually. $codec = $this->compute_codec(); switch(strtolower($codec)) { case 'x264': case 'h264': return strtolower($this->bitdepth) == '10 bits' ? 'h264 10-bit' : 'h264'; case 'h265': return 'h265'; case 'xvid': return 'XviD'; case 'divx': case 'divx 3': return 'DivX'; case 'mpeg-1': return 'MPEG'; case 'mpeg-2': return 'MPEG-2'; } switch(strtolower($this->codec)) { case 'wmv3': return 'VC-1'; case 'mp43': return 'MPEG-4 v3'; } switch(strtolower($this->videoformat)) { case 'vc-1': return 'VC-1'; case 's-mpeg 4 v3': return 'MPEG-4 v3'; } } protected function compute_form_resolution() { global $Resolutions; $closest = null; if (isset($this->height)) { $resolutions = $Resolutions; foreach($resolutions as $resolution) { if (!isset($closest) || abs($this->height - $resolution) < abs($this->height - $closest)) { $closest = $resolution; } } } return $closest; } }