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 /////////////////////////////////////////////////////////////////
11 // write.apetag.php //
12 // module for writing APE tags //
13 // dependencies: module.tag.apetag.php //
15 /////////////////////////////////////////////////////////////////
18 getid3_lib
::IncludeDependency(GETID3_INCLUDEPATH
.'module.tag.apetag.php', __FILE__
, true);
20 class getid3_write_apetag
33 * ReplayGain / MP3gain tags will be copied from old tag even if not passed in data.
37 public $always_preserve_replaygain = true;
40 * Any non-critical errors will be stored here.
44 public $warnings = array();
47 * Any critical errors will be stored here.
51 public $errors = array();
53 public function __construct() {
59 public function WriteAPEtag() {
60 // NOTE: All data passed to this function must be UTF-8 format
63 $ThisFileInfo = $getID3->analyze($this->filename
);
65 if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['lyrics3']['tag_offset_end'])) {
66 if ($ThisFileInfo['ape']['tag_offset_start'] >= $ThisFileInfo['lyrics3']['tag_offset_end']) {
67 // Current APE tag between Lyrics3 and ID3v1/EOF
68 // This break Lyrics3 functionality
69 if (!$this->DeleteAPEtag()) {
72 $ThisFileInfo = $getID3->analyze($this->filename
);
76 if ($this->always_preserve_replaygain
) {
77 $ReplayGainTagsToPreserve = array('mp3gain_minmax', 'mp3gain_album_minmax', 'mp3gain_undo', 'replaygain_track_peak', 'replaygain_track_gain', 'replaygain_album_peak', 'replaygain_album_gain');
78 foreach ($ReplayGainTagsToPreserve as $rg_key) {
79 if (isset($ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0]) && !isset($this->tag_data
[strtoupper($rg_key)][0])) {
80 $this->tag_data
[strtoupper($rg_key)][0] = $ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0];
85 if ($APEtag = $this->GenerateAPEtag()) {
86 if (getID3
::is_writable($this->filename
) && is_file($this->filename
) && ($fp = fopen($this->filename
, 'a+b'))) {
87 $oldignoreuserabort = ignore_user_abort(true);
90 $PostAPEdataOffset = $ThisFileInfo['avdataend'];
91 if (isset($ThisFileInfo['ape']['tag_offset_end'])) {
92 $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['ape']['tag_offset_end']);
94 if (isset($ThisFileInfo['lyrics3']['tag_offset_start'])) {
95 $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['lyrics3']['tag_offset_start']);
97 fseek($fp, $PostAPEdataOffset);
99 if ($ThisFileInfo['filesize'] > $PostAPEdataOffset) {
100 $PostAPEdata = fread($fp, $ThisFileInfo['filesize'] - $PostAPEdataOffset);
103 fseek($fp, $PostAPEdataOffset);
104 if (isset($ThisFileInfo['ape']['tag_offset_start'])) {
105 fseek($fp, $ThisFileInfo['ape']['tag_offset_start']);
107 ftruncate($fp, ftell($fp));
108 fwrite($fp, $APEtag, strlen($APEtag));
109 if (!empty($PostAPEdata)) {
110 fwrite($fp, $PostAPEdata, strlen($PostAPEdata));
114 ignore_user_abort($oldignoreuserabort);
124 public function DeleteAPEtag() {
125 $getID3 = new getID3
;
126 $ThisFileInfo = $getID3->analyze($this->filename
);
127 if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['ape']['tag_offset_end'])) {
128 if (getID3
::is_writable($this->filename
) && is_file($this->filename
) && ($fp = fopen($this->filename
, 'a+b'))) {
131 $oldignoreuserabort = ignore_user_abort(true);
133 fseek($fp, $ThisFileInfo['ape']['tag_offset_end']);
135 if ($ThisFileInfo['filesize'] > $ThisFileInfo['ape']['tag_offset_end']) {
136 $DataAfterAPE = fread($fp, $ThisFileInfo['filesize'] - $ThisFileInfo['ape']['tag_offset_end']);
139 ftruncate($fp, $ThisFileInfo['ape']['tag_offset_start']);
140 fseek($fp, $ThisFileInfo['ape']['tag_offset_start']);
142 if (!empty($DataAfterAPE)) {
143 fwrite($fp, $DataAfterAPE, strlen($DataAfterAPE));
148 ignore_user_abort($oldignoreuserabort);
158 * @return string|false
160 public function GenerateAPEtag() {
161 // NOTE: All data passed to this function must be UTF-8 format
164 if (!is_array($this->tag_data
)) {
167 foreach ($this->tag_data
as $key => $arrayofvalues) {
168 if (!is_array($arrayofvalues)) {
173 foreach ($arrayofvalues as $value) {
174 $valuestring .= str_replace("\x00", '', $value)."\x00";
176 $valuestring = rtrim($valuestring, "\x00");
178 // Length of the assigned value in bytes
179 $tagitem = getid3_lib
::LittleEndian2String(strlen($valuestring), 4);
181 //$tagitem .= $this->GenerateAPEtagFlags(true, true, false, 0, false);
182 $tagitem .= "\x00\x00\x00\x00";
184 $tagitem .= $this->CleanAPEtagItemKey($key)."\x00";
185 $tagitem .= $valuestring;
191 return $this->GenerateAPEtagHeaderFooter($items, true).implode('', $items).$this->GenerateAPEtagHeaderFooter($items, false);
195 * @param array $items
196 * @param bool $isheader
200 public function GenerateAPEtagHeaderFooter(&$items, $isheader=false) {
202 foreach ($items as $itemdata) {
203 $tagdatalength +
= strlen($itemdata);
206 $APEheader = 'APETAGEX';
207 $APEheader .= getid3_lib
::LittleEndian2String(2000, 4);
208 $APEheader .= getid3_lib
::LittleEndian2String(32 +
$tagdatalength, 4);
209 $APEheader .= getid3_lib
::LittleEndian2String(count($items), 4);
210 $APEheader .= $this->GenerateAPEtagFlags(true, true, $isheader, 0, false);
211 $APEheader .= str_repeat("\x00", 8);
217 * @param bool $header
218 * @param bool $footer
219 * @param bool $isheader
220 * @param int $encodingid
221 * @param bool $readonly
225 public function GenerateAPEtagFlags($header=true, $footer=true, $isheader=false, $encodingid=0, $readonly=false) {
226 $APEtagFlags = array_fill(0, 4, 0);
228 $APEtagFlags[0] |
= 0x80; // Tag contains a header
231 $APEtagFlags[0] |
= 0x40; // Tag contains no footer
234 $APEtagFlags[0] |
= 0x20; // This is the header, not the footer
237 // 0: Item contains text information coded in UTF-8
238 // 1: Item contains binary information °)
239 // 2: Item is a locator of external stored information °°)
241 $APEtagFlags[3] |
= ($encodingid << 1);
244 $APEtagFlags[3] |
= 0x01; // Tag or Item is Read Only
247 return chr($APEtagFlags[3]).chr($APEtagFlags[2]).chr($APEtagFlags[1]).chr($APEtagFlags[0]);
251 * @param string $itemkey
255 public function CleanAPEtagItemKey($itemkey) {
256 $itemkey = preg_replace("#[^\x20-\x7E]#i", '', $itemkey);
258 // http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html
259 switch (strtoupper($itemkey)) {
264 $itemkey = strtoupper($itemkey);
268 $itemkey = ucwords($itemkey);