[SPIP] v3.2.1-->v3.2.3
[lhc/web/www.git] / www / plugins-dist / medias / lib / getid3 / write.apetag.php
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.apetag.php //
12 // module for writing APE tags //
13 // dependencies: module.tag.apetag.php //
14 // ///
15 /////////////////////////////////////////////////////////////////
16
17
18 getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.apetag.php', __FILE__, true);
19
20 class getid3_write_apetag
21 {
22 /**
23 * @var string
24 */
25 public $filename;
26
27 /**
28 * @var array
29 */
30 public $tag_data;
31
32 /**
33 * ReplayGain / MP3gain tags will be copied from old tag even if not passed in data.
34 *
35 * @var bool
36 */
37 public $always_preserve_replaygain = true;
38
39 /**
40 * Any non-critical errors will be stored here.
41 *
42 * @var array
43 */
44 public $warnings = array();
45
46 /**
47 * Any critical errors will be stored here.
48 *
49 * @var array
50 */
51 public $errors = array();
52
53 public function __construct() {
54 }
55
56 /**
57 * @return bool
58 */
59 public function WriteAPEtag() {
60 // NOTE: All data passed to this function must be UTF-8 format
61
62 $getID3 = new getID3;
63 $ThisFileInfo = $getID3->analyze($this->filename);
64
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()) {
70 return false;
71 }
72 $ThisFileInfo = $getID3->analyze($this->filename);
73 }
74 }
75
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];
81 }
82 }
83 }
84
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);
88 flock($fp, LOCK_EX);
89
90 $PostAPEdataOffset = $ThisFileInfo['avdataend'];
91 if (isset($ThisFileInfo['ape']['tag_offset_end'])) {
92 $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['ape']['tag_offset_end']);
93 }
94 if (isset($ThisFileInfo['lyrics3']['tag_offset_start'])) {
95 $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['lyrics3']['tag_offset_start']);
96 }
97 fseek($fp, $PostAPEdataOffset);
98 $PostAPEdata = '';
99 if ($ThisFileInfo['filesize'] > $PostAPEdataOffset) {
100 $PostAPEdata = fread($fp, $ThisFileInfo['filesize'] - $PostAPEdataOffset);
101 }
102
103 fseek($fp, $PostAPEdataOffset);
104 if (isset($ThisFileInfo['ape']['tag_offset_start'])) {
105 fseek($fp, $ThisFileInfo['ape']['tag_offset_start']);
106 }
107 ftruncate($fp, ftell($fp));
108 fwrite($fp, $APEtag, strlen($APEtag));
109 if (!empty($PostAPEdata)) {
110 fwrite($fp, $PostAPEdata, strlen($PostAPEdata));
111 }
112 flock($fp, LOCK_UN);
113 fclose($fp);
114 ignore_user_abort($oldignoreuserabort);
115 return true;
116 }
117 }
118 return false;
119 }
120
121 /**
122 * @return bool
123 */
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'))) {
129
130 flock($fp, LOCK_EX);
131 $oldignoreuserabort = ignore_user_abort(true);
132
133 fseek($fp, $ThisFileInfo['ape']['tag_offset_end']);
134 $DataAfterAPE = '';
135 if ($ThisFileInfo['filesize'] > $ThisFileInfo['ape']['tag_offset_end']) {
136 $DataAfterAPE = fread($fp, $ThisFileInfo['filesize'] - $ThisFileInfo['ape']['tag_offset_end']);
137 }
138
139 ftruncate($fp, $ThisFileInfo['ape']['tag_offset_start']);
140 fseek($fp, $ThisFileInfo['ape']['tag_offset_start']);
141
142 if (!empty($DataAfterAPE)) {
143 fwrite($fp, $DataAfterAPE, strlen($DataAfterAPE));
144 }
145
146 flock($fp, LOCK_UN);
147 fclose($fp);
148 ignore_user_abort($oldignoreuserabort);
149
150 return true;
151 }
152 return false;
153 }
154 return true;
155 }
156
157 /**
158 * @return string|false
159 */
160 public function GenerateAPEtag() {
161 // NOTE: All data passed to this function must be UTF-8 format
162
163 $items = array();
164 if (!is_array($this->tag_data)) {
165 return false;
166 }
167 foreach ($this->tag_data as $key => $arrayofvalues) {
168 if (!is_array($arrayofvalues)) {
169 return false;
170 }
171
172 $valuestring = '';
173 foreach ($arrayofvalues as $value) {
174 $valuestring .= str_replace("\x00", '', $value)."\x00";
175 }
176 $valuestring = rtrim($valuestring, "\x00");
177
178 // Length of the assigned value in bytes
179 $tagitem = getid3_lib::LittleEndian2String(strlen($valuestring), 4);
180
181 //$tagitem .= $this->GenerateAPEtagFlags(true, true, false, 0, false);
182 $tagitem .= "\x00\x00\x00\x00";
183
184 $tagitem .= $this->CleanAPEtagItemKey($key)."\x00";
185 $tagitem .= $valuestring;
186
187 $items[] = $tagitem;
188
189 }
190
191 return $this->GenerateAPEtagHeaderFooter($items, true).implode('', $items).$this->GenerateAPEtagHeaderFooter($items, false);
192 }
193
194 /**
195 * @param array $items
196 * @param bool $isheader
197 *
198 * @return string
199 */
200 public function GenerateAPEtagHeaderFooter(&$items, $isheader=false) {
201 $tagdatalength = 0;
202 foreach ($items as $itemdata) {
203 $tagdatalength += strlen($itemdata);
204 }
205
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);
212
213 return $APEheader;
214 }
215
216 /**
217 * @param bool $header
218 * @param bool $footer
219 * @param bool $isheader
220 * @param int $encodingid
221 * @param bool $readonly
222 *
223 * @return string
224 */
225 public function GenerateAPEtagFlags($header=true, $footer=true, $isheader=false, $encodingid=0, $readonly=false) {
226 $APEtagFlags = array_fill(0, 4, 0);
227 if ($header) {
228 $APEtagFlags[0] |= 0x80; // Tag contains a header
229 }
230 if (!$footer) {
231 $APEtagFlags[0] |= 0x40; // Tag contains no footer
232 }
233 if ($isheader) {
234 $APEtagFlags[0] |= 0x20; // This is the header, not the footer
235 }
236
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 °°)
240 // 3: reserved
241 $APEtagFlags[3] |= ($encodingid << 1);
242
243 if ($readonly) {
244 $APEtagFlags[3] |= 0x01; // Tag or Item is Read Only
245 }
246
247 return chr($APEtagFlags[3]).chr($APEtagFlags[2]).chr($APEtagFlags[1]).chr($APEtagFlags[0]);
248 }
249
250 /**
251 * @param string $itemkey
252 *
253 * @return string
254 */
255 public function CleanAPEtagItemKey($itemkey) {
256 $itemkey = preg_replace("#[^\x20-\x7E]#i", '', $itemkey);
257
258 // http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html
259 switch (strtoupper($itemkey)) {
260 case 'EAN/UPC':
261 case 'ISBN':
262 case 'LC':
263 case 'ISRC':
264 $itemkey = strtoupper($itemkey);
265 break;
266
267 default:
268 $itemkey = ucwords($itemkey);
269 break;
270 }
271 return $itemkey;
272
273 }
274
275 }