1<?php 2 3///////////////////////////////////////////////////////////////// 4/// getID3() by James Heinrich <info@getid3.org> // 5// available at https://github.com/JamesHeinrich/getID3 // 6// or https://www.getid3.org // 7// or http://getid3.sourceforge.net // 8// see readme.txt for more details // 9///////////////////////////////////////////////////////////////// 10// // 11// write.metaflac.php // 12// module for writing metaflac tags // 13// dependencies: /helperapps/metaflac.exe // 14// /// 15///////////////////////////////////////////////////////////////// 16 17 18class getid3_write_metaflac 19{ 20 /** 21 * @var string 22 */ 23 public $filename; 24 25 /** 26 * @var array 27 */ 28 public $tag_data; 29 30 /** 31 * Any non-critical errors will be stored here. 32 * 33 * @var array 34 */ 35 public $warnings = array(); 36 37 /** 38 * Any critical errors will be stored here. 39 * 40 * @var array 41 */ 42 public $errors = array(); 43 44 private $pictures = array(); 45 46 public function __construct() { 47 } 48 49 /** 50 * @return bool 51 */ 52 public function WriteMetaFLAC() { 53 54 if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { 55 $this->errors[] = 'PHP running in Safe Mode (backtick operator not available) - cannot call metaflac, tags not written'; 56 return false; 57 } 58 59 $tempfilenames = array(); 60 61 62 if (!empty($this->tag_data['ATTACHED_PICTURE'])) { 63 foreach ($this->tag_data['ATTACHED_PICTURE'] as $key => $picturedetails) { 64 $temppicturefilename = tempnam(GETID3_TEMP_DIR, 'getID3'); 65 $tempfilenames[] = $temppicturefilename; 66 if (getID3::is_writable($temppicturefilename) && is_file($temppicturefilename) && ($fpcomments = fopen($temppicturefilename, 'wb'))) { 67 // https://xiph.org/flac/documentation_tools_flac.html#flac_options_picture 68 // [TYPE]|[MIME-TYPE]|[DESCRIPTION]|[WIDTHxHEIGHTxDEPTH[/COLORS]]|FILE 69 fwrite($fpcomments, $picturedetails['data']); 70 fclose($fpcomments); 71 $picture_typeid = (!empty($picturedetails['picturetypeid']) ? $this->ID3v2toFLACpictureTypes($picturedetails['picturetypeid']) : 3); // default to "3:Cover (front)" 72 $picture_mimetype = (!empty($picturedetails['mime']) ? $picturedetails['mime'] : ''); // should be auto-detected 73 $picture_width_height_depth = ''; 74 $this->pictures[] = $picture_typeid.'|'.$picture_mimetype.'|'.preg_replace('#[^\x20-\x7B\x7D-\x7F]#', '', $picturedetails['description']).'|'.$picture_width_height_depth.'|'.$temppicturefilename; 75 } else { 76 $this->errors[] = 'failed to open temporary tags file, tags not written - fopen("'.$temppicturefilename.'", "wb")'; 77 return false; 78 } 79 } 80 unset($this->tag_data['ATTACHED_PICTURE']); 81 } 82 83 84 // Create file with new comments 85 $tempcommentsfilename = tempnam(GETID3_TEMP_DIR, 'getID3'); 86 $tempfilenames[] = $tempcommentsfilename; 87 if (getID3::is_writable($tempcommentsfilename) && is_file($tempcommentsfilename) && ($fpcomments = fopen($tempcommentsfilename, 'wb'))) { 88 foreach ($this->tag_data as $key => $value) { 89 foreach ($value as $commentdata) { 90 fwrite($fpcomments, $this->CleanmetaflacName($key).'='.$commentdata."\n"); 91 } 92 } 93 fclose($fpcomments); 94 95 } else { 96 $this->errors[] = 'failed to open temporary tags file, tags not written - fopen("'.$tempcommentsfilename.'", "wb")'; 97 return false; 98 } 99 100 $oldignoreuserabort = ignore_user_abort(true); 101 if (GETID3_OS_ISWINDOWS) { 102 103 if (file_exists(GETID3_HELPERAPPSDIR.'metaflac.exe')) { 104 //$commandline = '"'.GETID3_HELPERAPPSDIR.'metaflac.exe" --no-utf8-convert --remove-all-tags --import-tags-from="'.$tempcommentsfilename.'" "'.str_replace('/', '\\', $this->filename).'"'; 105 // metaflac works fine if you copy-paste the above commandline into a command prompt, 106 // but refuses to work with `backtick` if there are "doublequotes" present around BOTH 107 // the metaflac pathname and the target filename. For whatever reason...?? 108 // The solution is simply ensure that the metaflac pathname has no spaces, 109 // and therefore does not need to be quoted 110 111 // On top of that, if error messages are not always captured properly under Windows 112 // To at least see if there was a problem, compare file modification timestamps before and after writing 113 clearstatcache(); 114 $timestampbeforewriting = filemtime($this->filename); 115 116 $commandline = GETID3_HELPERAPPSDIR.'metaflac.exe --no-utf8-convert --remove-all-tags --import-tags-from='.escapeshellarg($tempcommentsfilename); 117 foreach ($this->pictures as $picturecommand) { 118 $commandline .= ' --import-picture-from='.escapeshellarg($picturecommand); 119 } 120 $commandline .= ' '.escapeshellarg($this->filename).' 2>&1'; 121 $metaflacError = `$commandline`; 122 123 if (empty($metaflacError)) { 124 clearstatcache(); 125 if ($timestampbeforewriting == filemtime($this->filename)) { 126 $metaflacError = 'File modification timestamp has not changed - it looks like the tags were not written'; 127 } 128 } 129 } else { 130 $metaflacError = 'metaflac.exe not found in '.GETID3_HELPERAPPSDIR; 131 } 132 133 } else { 134 135 // It's simpler on *nix 136 $commandline = 'metaflac --no-utf8-convert --remove-all-tags --import-tags-from='.escapeshellarg($tempcommentsfilename); 137 foreach ($this->pictures as $picturecommand) { 138 $commandline .= ' --import-picture-from='.escapeshellarg($picturecommand); 139 } 140 $commandline .= ' '.escapeshellarg($this->filename).' 2>&1'; 141 $metaflacError = `$commandline`; 142 143 } 144 145 // Remove temporary comments file 146 foreach ($tempfilenames as $tempfilename) { 147 unlink($tempfilename); 148 } 149 ignore_user_abort($oldignoreuserabort); 150 151 if (!empty($metaflacError)) { 152 153 $this->errors[] = 'System call to metaflac failed with this message returned: '."\n\n".$metaflacError; 154 return false; 155 156 } 157 158 return true; 159 } 160 161 /** 162 * @return bool 163 */ 164 public function DeleteMetaFLAC() { 165 166 if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { 167 $this->errors[] = 'PHP running in Safe Mode (backtick operator not available) - cannot call metaflac, tags not deleted'; 168 return false; 169 } 170 171 $oldignoreuserabort = ignore_user_abort(true); 172 if (GETID3_OS_ISWINDOWS) { 173 174 if (file_exists(GETID3_HELPERAPPSDIR.'metaflac.exe')) { 175 // To at least see if there was a problem, compare file modification timestamps before and after writing 176 clearstatcache(); 177 $timestampbeforewriting = filemtime($this->filename); 178 179 $commandline = GETID3_HELPERAPPSDIR.'metaflac.exe --remove-all-tags "'.$this->filename.'" 2>&1'; 180 $metaflacError = `$commandline`; 181 182 if (empty($metaflacError)) { 183 clearstatcache(); 184 if ($timestampbeforewriting == filemtime($this->filename)) { 185 $metaflacError = 'File modification timestamp has not changed - it looks like the tags were not deleted'; 186 } 187 } 188 } else { 189 $metaflacError = 'metaflac.exe not found in '.GETID3_HELPERAPPSDIR; 190 } 191 192 } else { 193 194 // It's simpler on *nix 195 $commandline = 'metaflac --remove-all-tags "'.$this->filename.'" 2>&1'; 196 $metaflacError = `$commandline`; 197 198 } 199 200 ignore_user_abort($oldignoreuserabort); 201 202 if (!empty($metaflacError)) { 203 $this->errors[] = 'System call to metaflac failed with this message returned: '."\n\n".$metaflacError; 204 return false; 205 } 206 return true; 207 } 208 209 /** 210 * @param int $id3v2_picture_typeid 211 * 212 * @return int 213 */ 214 public function ID3v2toFLACpictureTypes($id3v2_picture_typeid) { 215 // METAFLAC picture type list is identical to ID3v2 picture type list (as least up to 0x14 "Publisher/Studio logotype") 216 // http://id3.org/id3v2.4.0-frames (section 4.14) 217 // https://xiph.org/flac/documentation_tools_flac.html#flac_options_picture 218 //return (isset($ID3v2toFLACpictureTypes[$id3v2_picture_typeid]) ? $ID3v2toFLACpictureTypes[$id3v2_picture_typeid] : 3); // default: "3: Cover (front)" 219 return (($id3v2_picture_typeid <= 0x14) ? $id3v2_picture_typeid : 3); // default: "3: Cover (front)" 220 } 221 222 /** 223 * @param string $originalcommentname 224 * 225 * @return string 226 */ 227 public function CleanmetaflacName($originalcommentname) { 228 // A case-insensitive field name that may consist of ASCII 0x20 through 0x7D, 0x3D ('=') excluded. 229 // ASCII 0x41 through 0x5A inclusive (A-Z) is to be considered equivalent to ASCII 0x61 through 230 // 0x7A inclusive (a-z). 231 232 // replace invalid chars with a space, return uppercase text 233 // Thanks Chris Bolt <chris-getid3Øbolt*cx> for improving this function 234 // note: *reg_replace() replaces nulls with empty string (not space) 235 return strtoupper(preg_replace('#[^ -<>-}]#', ' ', str_replace("\x00", ' ', $originalcommentname))); 236 } 237 238} 239