File indexing completed on 2024-05-12 17:26:02

0001 <?php
0002 /////////////////////////////////////////////////////////////////
0003 /// getID3() by James Heinrich <info@getid3.org>               //
0004 //  available at http://getid3.sourceforge.net                 //
0005 //            or http://www.getid3.org                         //
0006 //          also https://github.com/JamesHeinrich/getID3       //
0007 /////////////////////////////////////////////////////////////////
0008 // See readme.txt for more details                             //
0009 /////////////////////////////////////////////////////////////////
0010 //                                                             //
0011 // write.apetag.php                                            //
0012 // module for writing APE tags                                 //
0013 // dependencies: module.tag.apetag.php                         //
0014 //                                                            ///
0015 /////////////////////////////////////////////////////////////////
0016 
0017 
0018 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.apetag.php', __FILE__, true);
0019 
0020 class getid3_write_apetag
0021 {
0022 
0023   public $filename;
0024   public $tag_data;
0025   public $always_preserve_replaygain = true;    // ReplayGain / MP3gain tags will be copied from old tag even if not passed in data
0026   public $warnings                   = array(); // any non-critical errors will be stored here
0027   public $errors                     = array(); // any critical errors will be stored here
0028 
0029   public function getid3_write_apetag() {
0030     return true;
0031   }
0032 
0033   public function WriteAPEtag() {
0034     // NOTE: All data passed to this function must be UTF-8 format
0035 
0036     $getID3 = new getID3;
0037     $ThisFileInfo = $getID3->analyze($this->filename);
0038 
0039     if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['lyrics3']['tag_offset_end'])) {
0040       if ($ThisFileInfo['ape']['tag_offset_start'] >= $ThisFileInfo['lyrics3']['tag_offset_end']) {
0041         // Current APE tag between Lyrics3 and ID3v1/EOF
0042         // This break Lyrics3 functionality
0043         if (!$this->DeleteAPEtag()) {
0044           return false;
0045         }
0046         $ThisFileInfo = $getID3->analyze($this->filename);
0047       }
0048     }
0049 
0050     if ($this->always_preserve_replaygain) {
0051       $ReplayGainTagsToPreserve = array('mp3gain_minmax', 'mp3gain_album_minmax', 'mp3gain_undo', 'replaygain_track_peak', 'replaygain_track_gain', 'replaygain_album_peak', 'replaygain_album_gain');
0052       foreach ($ReplayGainTagsToPreserve as $rg_key) {
0053         if (isset($ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0]) && !isset($this->tag_data[strtoupper($rg_key)][0])) {
0054           $this->tag_data[strtoupper($rg_key)][0] = $ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0];
0055         }
0056       }
0057     }
0058 
0059     if ($APEtag = $this->GenerateAPEtag()) {
0060       if (is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'a+b'))) {
0061         $oldignoreuserabort = ignore_user_abort(true);
0062         flock($fp, LOCK_EX);
0063 
0064         $PostAPEdataOffset = $ThisFileInfo['avdataend'];
0065         if (isset($ThisFileInfo['ape']['tag_offset_end'])) {
0066           $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['ape']['tag_offset_end']);
0067         }
0068         if (isset($ThisFileInfo['lyrics3']['tag_offset_start'])) {
0069           $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['lyrics3']['tag_offset_start']);
0070         }
0071         fseek($fp, $PostAPEdataOffset);
0072         $PostAPEdata = '';
0073         if ($ThisFileInfo['filesize'] > $PostAPEdataOffset) {
0074           $PostAPEdata = fread($fp, $ThisFileInfo['filesize'] - $PostAPEdataOffset);
0075         }
0076 
0077         fseek($fp, $PostAPEdataOffset);
0078         if (isset($ThisFileInfo['ape']['tag_offset_start'])) {
0079           fseek($fp, $ThisFileInfo['ape']['tag_offset_start']);
0080         }
0081         ftruncate($fp, ftell($fp));
0082         fwrite($fp, $APEtag, strlen($APEtag));
0083         if (!empty($PostAPEdata)) {
0084           fwrite($fp, $PostAPEdata, strlen($PostAPEdata));
0085         }
0086         flock($fp, LOCK_UN);
0087         fclose($fp);
0088         ignore_user_abort($oldignoreuserabort);
0089         return true;
0090       }
0091     }
0092     return false;
0093   }
0094 
0095   public function DeleteAPEtag() {
0096     $getID3 = new getID3;
0097     $ThisFileInfo = $getID3->analyze($this->filename);
0098     if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['ape']['tag_offset_end'])) {
0099       if (is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'a+b'))) {
0100 
0101         flock($fp, LOCK_EX);
0102         $oldignoreuserabort = ignore_user_abort(true);
0103 
0104         fseek($fp, $ThisFileInfo['ape']['tag_offset_end']);
0105         $DataAfterAPE = '';
0106         if ($ThisFileInfo['filesize'] > $ThisFileInfo['ape']['tag_offset_end']) {
0107           $DataAfterAPE = fread($fp, $ThisFileInfo['filesize'] - $ThisFileInfo['ape']['tag_offset_end']);
0108         }
0109 
0110         ftruncate($fp, $ThisFileInfo['ape']['tag_offset_start']);
0111         fseek($fp, $ThisFileInfo['ape']['tag_offset_start']);
0112 
0113         if (!empty($DataAfterAPE)) {
0114           fwrite($fp, $DataAfterAPE, strlen($DataAfterAPE));
0115         }
0116 
0117         flock($fp, LOCK_UN);
0118         fclose($fp);
0119         ignore_user_abort($oldignoreuserabort);
0120 
0121         return true;
0122       }
0123       return false;
0124     }
0125     return true;
0126   }
0127 
0128 
0129   public function GenerateAPEtag() {
0130     // NOTE: All data passed to this function must be UTF-8 format
0131 
0132     $items = array();
0133     if (!is_array($this->tag_data)) {
0134       return false;
0135     }
0136     foreach ($this->tag_data as $key => $arrayofvalues) {
0137       if (!is_array($arrayofvalues)) {
0138         return false;
0139       }
0140 
0141       $valuestring = '';
0142       foreach ($arrayofvalues as $value) {
0143         $valuestring .= str_replace("\x00", '', $value)."\x00";
0144       }
0145       $valuestring = rtrim($valuestring, "\x00");
0146 
0147       // Length of the assigned value in bytes
0148       $tagitem  = getid3_lib::LittleEndian2String(strlen($valuestring), 4);
0149 
0150       //$tagitem .= $this->GenerateAPEtagFlags(true, true, false, 0, false);
0151       $tagitem .= "\x00\x00\x00\x00";
0152 
0153       $tagitem .= $this->CleanAPEtagItemKey($key)."\x00";
0154       $tagitem .= $valuestring;
0155 
0156       $items[] = $tagitem;
0157 
0158     }
0159 
0160     return $this->GenerateAPEtagHeaderFooter($items, true).implode('', $items).$this->GenerateAPEtagHeaderFooter($items, false);
0161   }
0162 
0163   public function GenerateAPEtagHeaderFooter(&$items, $isheader=false) {
0164     $tagdatalength = 0;
0165     foreach ($items as $itemdata) {
0166       $tagdatalength += strlen($itemdata);
0167     }
0168 
0169     $APEheader  = 'APETAGEX';
0170     $APEheader .= getid3_lib::LittleEndian2String(2000, 4);
0171     $APEheader .= getid3_lib::LittleEndian2String(32 + $tagdatalength, 4);
0172     $APEheader .= getid3_lib::LittleEndian2String(count($items), 4);
0173     $APEheader .= $this->GenerateAPEtagFlags(true, true, $isheader, 0, false);
0174     $APEheader .= str_repeat("\x00", 8);
0175 
0176     return $APEheader;
0177   }
0178 
0179   public function GenerateAPEtagFlags($header=true, $footer=true, $isheader=false, $encodingid=0, $readonly=false) {
0180     $APEtagFlags = array_fill(0, 4, 0);
0181     if ($header) {
0182       $APEtagFlags[0] |= 0x80; // Tag contains a header
0183     }
0184     if (!$footer) {
0185       $APEtagFlags[0] |= 0x40; // Tag contains no footer
0186     }
0187     if ($isheader) {
0188       $APEtagFlags[0] |= 0x20; // This is the header, not the footer
0189     }
0190 
0191     // 0: Item contains text information coded in UTF-8
0192     // 1: Item contains binary information °)
0193     // 2: Item is a locator of external stored information °°)
0194     // 3: reserved
0195     $APEtagFlags[3] |= ($encodingid << 1);
0196 
0197     if ($readonly) {
0198       $APEtagFlags[3] |= 0x01; // Tag or Item is Read Only
0199     }
0200 
0201     return chr($APEtagFlags[3]).chr($APEtagFlags[2]).chr($APEtagFlags[1]).chr($APEtagFlags[0]);
0202   }
0203 
0204   public function CleanAPEtagItemKey($itemkey) {
0205     $itemkey = preg_replace("#[^\x20-\x7E]#i", '', $itemkey);
0206 
0207     // http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html
0208     switch (strtoupper($itemkey)) {
0209       case 'EAN/UPC':
0210       case 'ISBN':
0211       case 'LC':
0212       case 'ISRC':
0213         $itemkey = strtoupper($itemkey);
0214         break;
0215 
0216       default:
0217         $itemkey = ucwords($itemkey);
0218         break;
0219     }
0220     return $itemkey;
0221 
0222   }
0223 
0224 }