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.php                                                   //
12// module for writing tags (APEv2, ID3v1, ID3v2)               //
13// dependencies: getid3.lib.php                                //
14//               write.apetag.php (optional)                   //
15//               write.id3v1.php (optional)                    //
16//               write.id3v2.php (optional)                    //
17//               write.vorbiscomment.php (optional)            //
18//               write.metaflac.php (optional)                 //
19//               write.lyrics3.php (optional)                  //
20//                                                            ///
21/////////////////////////////////////////////////////////////////
22
23if (!defined('GETID3_INCLUDEPATH')) {
24	throw new Exception('getid3.php MUST be included before calling getid3_writetags');
25}
26if (!include_once(GETID3_INCLUDEPATH.'getid3.lib.php')) {
27	throw new Exception('write.php depends on getid3.lib.php, which is missing.');
28}
29
30/**
31 * NOTES:
32 *
33 * You should pass data here with standard field names as follows:
34 * * TITLE
35 * * ARTIST
36 * * ALBUM
37 * * TRACKNUMBER
38 * * COMMENT
39 * * GENRE
40 * * YEAR
41 * * ATTACHED_PICTURE (ID3v2 only)
42 * The APEv2 Tag Items Keys definition says "TRACK" is correct but foobar2000 uses "TRACKNUMBER" instead
43 * Pass data here as "TRACKNUMBER" for compatability with all formats
44 *
45 * @link http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html
46 */
47class getid3_writetags
48{
49	/**
50	 * Absolute filename of file to write tags to.
51	 *
52	 * @var string
53	 */
54	public $filename;
55
56	/**
57	 * Array of tag formats to write ('id3v1', 'id3v2.2', 'id2v2.3', 'id3v2.4', 'ape', 'vorbiscomment',
58	 * 'metaflac', 'real').
59	 *
60	 * @var array
61	 */
62	public $tagformats         = array();
63
64	/**
65	 * 2-dimensional array of tag data (ex: $data['ARTIST'][0] = 'Elvis').
66	 *
67	 * @var array
68	 */
69	public $tag_data           = array(array());
70
71	/**
72	 * Text encoding used for tag data ('ISO-8859-1', 'UTF-8', 'UTF-16', 'UTF-16LE', 'UTF-16BE', ).
73	 *
74	 * @var string
75	 */
76	public $tag_encoding       = 'ISO-8859-1';
77
78	/**
79	 * If true will erase existing tag data and write only passed data; if false will merge passed data
80	 * with existing tag data.
81	 *
82	 * @var bool
83	 */
84	public $overwrite_tags     = true;
85
86	/**
87	 * If true will erase remove all existing tags and only write those passed in $tagformats;
88	 * If false will ignore any tags not mentioned in $tagformats.
89	 *
90	 * @var bool
91	 */
92	public $remove_other_tags  = false;
93
94	/**
95	 * ISO-639-2 3-character language code needed for some ID3v2 frames.
96	 *
97	 * @link http://www.id3.org/iso639-2.html
98	 *
99	 * @var string
100	 */
101	public $id3v2_tag_language = 'eng';
102
103	/**
104	 * Minimum length of ID3v2 tags (will be padded to this length if tag data is shorter).
105	 *
106	 * @var int
107	 */
108	public $id3v2_paddedlength = 4096;
109
110	/**
111	 * Any non-critical errors will be stored here.
112	 *
113	 * @var array
114	 */
115	public $warnings           = array();
116
117	/**
118	 * Any critical errors will be stored here.
119	 *
120	 * @var array
121	 */
122	public $errors             = array();
123
124	/**
125	 * Analysis of file before writing.
126	 *
127	 * @var array
128	 */
129	private $ThisFileInfo;
130
131	public function __construct() {
132	}
133
134	/**
135	 * @return bool
136	 */
137	public function WriteTags() {
138
139		if (empty($this->filename)) {
140			$this->errors[] = 'filename is undefined in getid3_writetags';
141			return false;
142		} elseif (!file_exists($this->filename)) {
143			$this->errors[] = 'filename set to non-existant file "'.$this->filename.'" in getid3_writetags';
144			return false;
145		}
146
147		if (!is_array($this->tagformats)) {
148			$this->errors[] = 'tagformats must be an array in getid3_writetags';
149			return false;
150		}
151		// prevent duplicate tag formats
152		$this->tagformats = array_unique($this->tagformats);
153
154		// prevent trying to specify more than one version of ID3v2 tag to write simultaneously
155		$id3typecounter = 0;
156		foreach ($this->tagformats as $tagformat) {
157			if (substr(strtolower($tagformat), 0, 6) == 'id3v2.') {
158				$id3typecounter++;
159			}
160		}
161		if ($id3typecounter > 1) {
162			$this->errors[] = 'tagformats must not contain more than one version of ID3v2';
163			return false;
164		}
165
166		$TagFormatsToRemove = array();
167		$AllowedTagFormats = array();
168		if (filesize($this->filename) == 0) {
169
170			// empty file special case - allow any tag format, don't check existing format
171			// could be useful if you want to generate tag data for a non-existant file
172			$this->ThisFileInfo = array('fileformat'=>'');
173			$AllowedTagFormats = array('id3v1', 'id3v2.2', 'id3v2.3', 'id3v2.4', 'ape', 'lyrics3');
174
175		} else {
176
177			$getID3 = new getID3;
178			$getID3->encoding = $this->tag_encoding;
179			$this->ThisFileInfo = $getID3->analyze($this->filename);
180
181			// check for what file types are allowed on this fileformat
182			switch (isset($this->ThisFileInfo['fileformat']) ? $this->ThisFileInfo['fileformat'] : '') {
183				case 'mp3':
184				case 'mp2':
185				case 'mp1':
186				case 'riff': // maybe not officially, but people do it anyway
187					$AllowedTagFormats = array('id3v1', 'id3v2.2', 'id3v2.3', 'id3v2.4', 'ape', 'lyrics3');
188					break;
189
190				case 'mpc':
191					$AllowedTagFormats = array('ape');
192					break;
193
194				case 'flac':
195					$AllowedTagFormats = array('metaflac');
196					break;
197
198				case 'real':
199					$AllowedTagFormats = array('real');
200					break;
201
202				case 'ogg':
203					switch (isset($this->ThisFileInfo['audio']['dataformat']) ? $this->ThisFileInfo['audio']['dataformat'] : '') {
204						case 'flac':
205							//$AllowedTagFormats = array('metaflac');
206							$this->errors[] = 'metaflac is not (yet) compatible with OggFLAC files';
207							return false;
208						case 'vorbis':
209							$AllowedTagFormats = array('vorbiscomment');
210							break;
211						default:
212							$this->errors[] = 'metaflac is not (yet) compatible with Ogg files other than OggVorbis';
213							return false;
214					}
215					break;
216
217				default:
218					$AllowedTagFormats = array();
219					break;
220			}
221			foreach ($this->tagformats as $requested_tag_format) {
222				if (!in_array($requested_tag_format, $AllowedTagFormats)) {
223					$errormessage = 'Tag format "'.$requested_tag_format.'" is not allowed on "'.(isset($this->ThisFileInfo['fileformat']) ? $this->ThisFileInfo['fileformat'] : '');
224					$errormessage .= (isset($this->ThisFileInfo['audio']['dataformat']) ? '.'.$this->ThisFileInfo['audio']['dataformat'] : '');
225					$errormessage .= '" files';
226					$this->errors[] = $errormessage;
227					return false;
228				}
229			}
230
231			// List of other tag formats, removed if requested
232			if ($this->remove_other_tags) {
233				foreach ($AllowedTagFormats as $AllowedTagFormat) {
234					switch ($AllowedTagFormat) {
235						case 'id3v2.2':
236						case 'id3v2.3':
237						case 'id3v2.4':
238							if (!in_array('id3v2', $TagFormatsToRemove) && !in_array('id3v2.2', $this->tagformats) && !in_array('id3v2.3', $this->tagformats) && !in_array('id3v2.4', $this->tagformats)) {
239								$TagFormatsToRemove[] = 'id3v2';
240							}
241							break;
242
243						default:
244							if (!in_array($AllowedTagFormat, $this->tagformats)) {
245								$TagFormatsToRemove[] = $AllowedTagFormat;
246							}
247							break;
248					}
249				}
250			}
251		}
252
253		$WritingFilesToInclude = array_merge($this->tagformats, $TagFormatsToRemove);
254
255		// Check for required include files and include them
256		foreach ($WritingFilesToInclude as $tagformat) {
257			switch ($tagformat) {
258				case 'ape':
259					$GETID3_ERRORARRAY = &$this->errors;
260					getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'write.apetag.php', __FILE__, true);
261					break;
262
263				case 'id3v1':
264				case 'lyrics3':
265				case 'vorbiscomment':
266				case 'metaflac':
267				case 'real':
268					$GETID3_ERRORARRAY = &$this->errors;
269					getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'write.'.$tagformat.'.php', __FILE__, true);
270					break;
271
272				case 'id3v2.2':
273				case 'id3v2.3':
274				case 'id3v2.4':
275				case 'id3v2':
276					$GETID3_ERRORARRAY = &$this->errors;
277					getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'write.id3v2.php', __FILE__, true);
278					break;
279
280				default:
281					$this->errors[] = 'unknown tag format "'.$tagformat.'" in $tagformats in WriteTags()';
282					return false;
283			}
284
285		}
286
287		// Validation of supplied data
288		if (!is_array($this->tag_data)) {
289			$this->errors[] = '$this->tag_data is not an array in WriteTags()';
290			return false;
291		}
292		// convert supplied data array keys to upper case, if they're not already
293		foreach ($this->tag_data as $tag_key => $tag_array) {
294			if (strtoupper($tag_key) !== $tag_key) {
295				$this->tag_data[strtoupper($tag_key)] = $this->tag_data[$tag_key];
296				unset($this->tag_data[$tag_key]);
297			}
298		}
299		// convert source data array keys to upper case, if they're not already
300		if (!empty($this->ThisFileInfo['tags'])) {
301			foreach ($this->ThisFileInfo['tags'] as $tag_format => $tag_data_array) {
302				foreach ($tag_data_array as $tag_key => $tag_array) {
303					if (strtoupper($tag_key) !== $tag_key) {
304						$this->ThisFileInfo['tags'][$tag_format][strtoupper($tag_key)] = $this->ThisFileInfo['tags'][$tag_format][$tag_key];
305						unset($this->ThisFileInfo['tags'][$tag_format][$tag_key]);
306					}
307				}
308			}
309		}
310
311		// Convert "TRACK" to "TRACK_NUMBER" (if needed) for compatability with all formats
312		if (isset($this->tag_data['TRACK']) && !isset($this->tag_data['TRACK_NUMBER'])) {
313			$this->tag_data['TRACK_NUMBER'] = $this->tag_data['TRACK'];
314			unset($this->tag_data['TRACK']);
315		}
316
317		// Remove all other tag formats, if requested
318		if ($this->remove_other_tags) {
319			$this->DeleteTags($TagFormatsToRemove);
320		}
321
322		// Write data for each tag format
323		foreach ($this->tagformats as $tagformat) {
324			$success = false; // overridden if tag writing is successful
325			switch ($tagformat) {
326				case 'ape':
327					$ape_writer = new getid3_write_apetag;
328					if ($ape_writer->tag_data = $this->FormatDataForAPE()) {
329						$ape_writer->filename = $this->filename;
330						if (($success = $ape_writer->WriteAPEtag()) === false) {
331							$this->errors[] = 'WriteAPEtag() failed with message(s):<pre><ul><li>'.str_replace("\n", '</li><li>', htmlentities(trim(implode("\n", $ape_writer->errors)))).'</li></ul></pre>';
332						}
333					} else {
334						$this->errors[] = 'FormatDataForAPE() failed';
335					}
336					break;
337
338				case 'id3v1':
339					$id3v1_writer = new getid3_write_id3v1;
340					if ($id3v1_writer->tag_data = $this->FormatDataForID3v1()) {
341						$id3v1_writer->filename = $this->filename;
342						if (($success = $id3v1_writer->WriteID3v1()) === false) {
343							$this->errors[] = 'WriteID3v1() failed with message(s):<pre><ul><li>'.str_replace("\n", '</li><li>', htmlentities(trim(implode("\n", $id3v1_writer->errors)))).'</li></ul></pre>';
344						}
345					} else {
346						$this->errors[] = 'FormatDataForID3v1() failed';
347					}
348					break;
349
350				case 'id3v2.2':
351				case 'id3v2.3':
352				case 'id3v2.4':
353					$id3v2_writer = new getid3_write_id3v2;
354					$id3v2_writer->majorversion = intval(substr($tagformat, -1));
355					$id3v2_writer->paddedlength = $this->id3v2_paddedlength;
356					$id3v2_writer_tag_data = $this->FormatDataForID3v2($id3v2_writer->majorversion);
357					if ($id3v2_writer_tag_data !== false) {
358						$id3v2_writer->tag_data = $id3v2_writer_tag_data;
359						unset($id3v2_writer_tag_data);
360						$id3v2_writer->filename = $this->filename;
361						if (($success = $id3v2_writer->WriteID3v2()) === false) {
362							$this->errors[] = 'WriteID3v2() failed with message(s):<pre><ul><li>'.str_replace("\n", '</li><li>', htmlentities(trim(implode("\n", $id3v2_writer->errors)))).'</li></ul></pre>';
363						}
364					} else {
365						$this->errors[] = 'FormatDataForID3v2() failed';
366					}
367					break;
368
369				case 'vorbiscomment':
370					$vorbiscomment_writer = new getid3_write_vorbiscomment;
371					if ($vorbiscomment_writer->tag_data = $this->FormatDataForVorbisComment()) {
372						$vorbiscomment_writer->filename = $this->filename;
373						if (($success = $vorbiscomment_writer->WriteVorbisComment()) === false) {
374							$this->errors[] = 'WriteVorbisComment() failed with message(s):<pre><ul><li>'.str_replace("\n", '</li><li>', htmlentities(trim(implode("\n", $vorbiscomment_writer->errors)))).'</li></ul></pre>';
375						}
376					} else {
377						$this->errors[] = 'FormatDataForVorbisComment() failed';
378					}
379					break;
380
381				case 'metaflac':
382					$metaflac_writer = new getid3_write_metaflac;
383					if ($metaflac_writer->tag_data = $this->FormatDataForMetaFLAC()) {
384						$metaflac_writer->filename = $this->filename;
385						if (($success = $metaflac_writer->WriteMetaFLAC()) === false) {
386							$this->errors[] = 'WriteMetaFLAC() failed with message(s):<pre><ul><li>'.str_replace("\n", '</li><li>', htmlentities(trim(implode("\n", $metaflac_writer->errors)))).'</li></ul></pre>';
387						}
388					} else {
389						$this->errors[] = 'FormatDataForMetaFLAC() failed';
390					}
391					break;
392
393				case 'real':
394					$real_writer = new getid3_write_real;
395					if ($real_writer->tag_data = $this->FormatDataForReal()) {
396						$real_writer->filename = $this->filename;
397						if (($success = $real_writer->WriteReal()) === false) {
398							$this->errors[] = 'WriteReal() failed with message(s):<pre><ul><li>'.str_replace("\n", '</li><li>', htmlentities(trim(implode("\n", $real_writer->errors)))).'</li></ul></pre>';
399						}
400					} else {
401						$this->errors[] = 'FormatDataForReal() failed';
402					}
403					break;
404
405				default:
406					$this->errors[] = 'Invalid tag format to write: "'.$tagformat.'"';
407					return false;
408			}
409			if (!$success) {
410				return false;
411			}
412		}
413		return true;
414
415	}
416
417	/**
418	 * @param string[] $TagFormatsToDelete
419	 *
420	 * @return bool
421	 */
422	public function DeleteTags($TagFormatsToDelete) {
423		foreach ($TagFormatsToDelete as $DeleteTagFormat) {
424			$success = false; // overridden if tag deletion is successful
425			switch ($DeleteTagFormat) {
426				case 'id3v1':
427					$id3v1_writer = new getid3_write_id3v1;
428					$id3v1_writer->filename = $this->filename;
429					if (($success = $id3v1_writer->RemoveID3v1()) === false) {
430						$this->errors[] = 'RemoveID3v1() failed with message(s):<PRE><UL><LI>'.trim(implode('</LI><LI>', $id3v1_writer->errors)).'</LI></UL></PRE>';
431					}
432					break;
433
434				case 'id3v2':
435					$id3v2_writer = new getid3_write_id3v2;
436					$id3v2_writer->filename = $this->filename;
437					if (($success = $id3v2_writer->RemoveID3v2()) === false) {
438						$this->errors[] = 'RemoveID3v2() failed with message(s):<PRE><UL><LI>'.trim(implode('</LI><LI>', $id3v2_writer->errors)).'</LI></UL></PRE>';
439					}
440					break;
441
442				case 'ape':
443					$ape_writer = new getid3_write_apetag;
444					$ape_writer->filename = $this->filename;
445					if (($success = $ape_writer->DeleteAPEtag()) === false) {
446						$this->errors[] = 'DeleteAPEtag() failed with message(s):<PRE><UL><LI>'.trim(implode('</LI><LI>', $ape_writer->errors)).'</LI></UL></PRE>';
447					}
448					break;
449
450				case 'vorbiscomment':
451					$vorbiscomment_writer = new getid3_write_vorbiscomment;
452					$vorbiscomment_writer->filename = $this->filename;
453					if (($success = $vorbiscomment_writer->DeleteVorbisComment()) === false) {
454						$this->errors[] = 'DeleteVorbisComment() failed with message(s):<PRE><UL><LI>'.trim(implode('</LI><LI>', $vorbiscomment_writer->errors)).'</LI></UL></PRE>';
455					}
456					break;
457
458				case 'metaflac':
459					$metaflac_writer = new getid3_write_metaflac;
460					$metaflac_writer->filename = $this->filename;
461					if (($success = $metaflac_writer->DeleteMetaFLAC()) === false) {
462						$this->errors[] = 'DeleteMetaFLAC() failed with message(s):<PRE><UL><LI>'.trim(implode('</LI><LI>', $metaflac_writer->errors)).'</LI></UL></PRE>';
463					}
464					break;
465
466				case 'lyrics3':
467					$lyrics3_writer = new getid3_write_lyrics3;
468					$lyrics3_writer->filename = $this->filename;
469					if (($success = $lyrics3_writer->DeleteLyrics3()) === false) {
470						$this->errors[] = 'DeleteLyrics3() failed with message(s):<PRE><UL><LI>'.trim(implode('</LI><LI>', $lyrics3_writer->errors)).'</LI></UL></PRE>';
471					}
472					break;
473
474				case 'real':
475					$real_writer = new getid3_write_real;
476					$real_writer->filename = $this->filename;
477					if (($success = $real_writer->RemoveReal()) === false) {
478						$this->errors[] = 'RemoveReal() failed with message(s):<PRE><UL><LI>'.trim(implode('</LI><LI>', $real_writer->errors)).'</LI></UL></PRE>';
479					}
480					break;
481
482				default:
483					$this->errors[] = 'Invalid tag format to delete: "'.$DeleteTagFormat.'"';
484					return false;
485			}
486			if (!$success) {
487				return false;
488			}
489		}
490		return true;
491	}
492
493	/**
494	 * @param string $TagFormat
495	 * @param array  $tag_data
496	 *
497	 * @return bool
498	 * @throws Exception
499	 */
500	public function MergeExistingTagData($TagFormat, &$tag_data) {
501		// Merge supplied data with existing data, if requested
502		if ($this->overwrite_tags) {
503			// do nothing - ignore previous data
504		} else {
505			throw new Exception('$this->overwrite_tags=false is known to be buggy in this version of getID3. Check http://github.com/JamesHeinrich/getID3 for a newer version.');
506//			if (!isset($this->ThisFileInfo['tags'][$TagFormat])) {
507//				return false;
508//			}
509//			$tag_data = array_merge_recursive($tag_data, $this->ThisFileInfo['tags'][$TagFormat]);
510		}
511		return true;
512	}
513
514	/**
515	 * @return array
516	 */
517	public function FormatDataForAPE() {
518		$ape_tag_data = array();
519		foreach ($this->tag_data as $tag_key => $valuearray) {
520			switch ($tag_key) {
521				case 'ATTACHED_PICTURE':
522					// ATTACHED_PICTURE is ID3v2 only - ignore
523					$this->warnings[] = '$data['.$tag_key.'] is assumed to be ID3v2 APIC data - NOT written to APE tag';
524					break;
525
526				default:
527					foreach ($valuearray as $key => $value) {
528						if (is_string($value) || is_numeric($value)) {
529							$ape_tag_data[$tag_key][$key] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $value);
530						} else {
531							$this->warnings[] = '$data['.$tag_key.']['.$key.'] is not a string value - all of $data['.$tag_key.'] NOT written to APE tag';
532							unset($ape_tag_data[$tag_key]);
533							break;
534						}
535					}
536					break;
537			}
538		}
539		$this->MergeExistingTagData('ape', $ape_tag_data);
540		return $ape_tag_data;
541	}
542
543	/**
544	 * @return array
545	 */
546	public function FormatDataForID3v1() {
547		$tag_data_id3v1            = array();
548		$tag_data_id3v1['genreid'] = 255;
549		if (!empty($this->tag_data['GENRE'])) {
550			foreach ($this->tag_data['GENRE'] as $key => $value) {
551				if (getid3_id3v1::LookupGenreID($value) !== false) {
552					$tag_data_id3v1['genreid'] = getid3_id3v1::LookupGenreID($value);
553					break;
554				}
555			}
556		}
557		$tag_data_id3v1['title']        =        getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['TITLE']       ) ? $this->tag_data['TITLE']        : array())));
558		$tag_data_id3v1['artist']       =        getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['ARTIST']      ) ? $this->tag_data['ARTIST']       : array())));
559		$tag_data_id3v1['album']        =        getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['ALBUM']       ) ? $this->tag_data['ALBUM']        : array())));
560		$tag_data_id3v1['year']         =        getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['YEAR']        ) ? $this->tag_data['YEAR']         : array())));
561		$tag_data_id3v1['comment']      =        getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['COMMENT']     ) ? $this->tag_data['COMMENT']      : array())));
562		$tag_data_id3v1['track_number'] = intval(getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['TRACK_NUMBER']) ? $this->tag_data['TRACK_NUMBER'] : array()))));
563		if ($tag_data_id3v1['track_number'] <= 0) {
564			$tag_data_id3v1['track_number'] = '';
565		}
566
567		$this->MergeExistingTagData('id3v1', $tag_data_id3v1);
568		return $tag_data_id3v1;
569	}
570
571	/**
572	 * @param int $id3v2_majorversion
573	 *
574	 * @return array|false
575	 */
576	public function FormatDataForID3v2($id3v2_majorversion) {
577		$tag_data_id3v2 = array();
578
579		$ID3v2_text_encoding_lookup[2] = array('ISO-8859-1'=>0, 'UTF-16'=>1);
580		$ID3v2_text_encoding_lookup[3] = array('ISO-8859-1'=>0, 'UTF-16'=>1);
581		$ID3v2_text_encoding_lookup[4] = array('ISO-8859-1'=>0, 'UTF-16'=>1, 'UTF-16BE'=>2, 'UTF-8'=>3);
582		foreach ($this->tag_data as $tag_key => $valuearray) {
583			$ID3v2_framename = getid3_write_id3v2::ID3v2ShortFrameNameLookup($id3v2_majorversion, $tag_key);
584			switch ($ID3v2_framename) {
585				case 'APIC':
586					foreach ($valuearray as $key => $apic_data_array) {
587						if (isset($apic_data_array['data']) &&
588							isset($apic_data_array['picturetypeid']) &&
589							isset($apic_data_array['description']) &&
590							isset($apic_data_array['mime'])) {
591								$tag_data_id3v2['APIC'][] = $apic_data_array;
592						} else {
593							$this->errors[] = 'ID3v2 APIC data is not properly structured';
594							return false;
595						}
596					}
597					break;
598
599				case 'POPM':
600					if (isset($valuearray['email']) &&
601						isset($valuearray['rating']) &&
602						isset($valuearray['data'])) {
603							$tag_data_id3v2['POPM'][] = $valuearray;
604					} else {
605						$this->errors[] = 'ID3v2 POPM data is not properly structured';
606						return false;
607					}
608					break;
609
610				case 'GRID':
611					if (
612						isset($valuearray['groupsymbol']) &&
613						isset($valuearray['ownerid']) &&
614						isset($valuearray['data'])
615					) {
616							$tag_data_id3v2['GRID'][] = $valuearray;
617					} else {
618						$this->errors[] = 'ID3v2 GRID data is not properly structured';
619						return false;
620					}
621					break;
622
623				case 'UFID':
624					if (isset($valuearray['ownerid']) &&
625						isset($valuearray['data'])) {
626							$tag_data_id3v2['UFID'][] = $valuearray;
627					} else {
628						$this->errors[] = 'ID3v2 UFID data is not properly structured';
629						return false;
630					}
631					break;
632
633				case 'TXXX':
634					foreach ($valuearray as $key => $txxx_data_array) {
635						if (isset($txxx_data_array['description']) && isset($txxx_data_array['data'])) {
636							$tag_data_id3v2['TXXX'][] = $txxx_data_array;
637						} else {
638							$this->errors[] = 'ID3v2 TXXX data is not properly structured';
639							return false;
640						}
641					}
642					break;
643
644				case '':
645					$this->errors[] = 'ID3v2: Skipping "'.$tag_key.'" because cannot match it to a known ID3v2 frame type';
646					// some other data type, don't know how to handle it, ignore it
647					break;
648
649				default:
650					// most other (text) frames can be copied over as-is
651					foreach ($valuearray as $key => $value) {
652						if (isset($ID3v2_text_encoding_lookup[$id3v2_majorversion][$this->tag_encoding])) {
653							// source encoding is valid in ID3v2 - use it with no conversion
654							$tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = $ID3v2_text_encoding_lookup[$id3v2_majorversion][$this->tag_encoding];
655							$tag_data_id3v2[$ID3v2_framename][$key]['data']       = $value;
656						} else {
657							// source encoding is NOT valid in ID3v2 - convert it to an ID3v2-valid encoding first
658							if ($id3v2_majorversion < 4) {
659								// convert data from other encoding to UTF-16 (with BOM)
660								// note: some software, notably Windows Media Player and iTunes are broken and treat files tagged with UTF-16BE (with BOM) as corrupt
661								// therefore we force data to UTF-16LE and manually prepend the BOM
662								$ID3v2_tag_data_converted = false;
663								if (/*!$ID3v2_tag_data_converted && */($this->tag_encoding == 'ISO-8859-1')) {
664									// great, leave data as-is for minimum compatability problems
665									$tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 0;
666									$tag_data_id3v2[$ID3v2_framename][$key]['data']       = $value;
667									$ID3v2_tag_data_converted = true;
668								}
669								if (!$ID3v2_tag_data_converted && ($this->tag_encoding == 'UTF-8')) {
670									do {
671										// if UTF-8 string does not include any characters above chr(127) then it is identical to ISO-8859-1
672										for ($i = 0; $i < strlen($value); $i++) {
673											if (ord($value[$i]) > 127) {
674												break 2;
675											}
676										}
677										$tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 0;
678										$tag_data_id3v2[$ID3v2_framename][$key]['data']       = $value;
679										$ID3v2_tag_data_converted = true;
680									} while (false);
681								}
682								if (!$ID3v2_tag_data_converted) {
683									$tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 1;
684									//$tag_data_id3v2[$ID3v2_framename][$key]['data']       = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-16', $value); // output is UTF-16LE+BOM or UTF-16BE+BOM depending on system architecture
685									$tag_data_id3v2[$ID3v2_framename][$key]['data']       = "\xFF\xFE".getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-16LE', $value); // force LittleEndian order version of UTF-16
686									$ID3v2_tag_data_converted = true;
687								}
688
689							} else {
690								// convert data from other encoding to UTF-8
691								$tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 3;
692								$tag_data_id3v2[$ID3v2_framename][$key]['data']       = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $value);
693							}
694						}
695
696						// These values are not needed for all frame types, but if they're not used no matter
697						$tag_data_id3v2[$ID3v2_framename][$key]['description'] = '';
698						$tag_data_id3v2[$ID3v2_framename][$key]['language']    = $this->id3v2_tag_language;
699					}
700					break;
701			}
702		}
703		$this->MergeExistingTagData('id3v2', $tag_data_id3v2);
704		return $tag_data_id3v2;
705	}
706
707	/**
708	 * @return array
709	 */
710	public function FormatDataForVorbisComment() {
711		$tag_data_vorbiscomment = $this->tag_data;
712
713		// check for multi-line comment values - split out to multiple comments if neccesary
714		// and convert data to UTF-8 strings
715		foreach ($tag_data_vorbiscomment as $tag_key => $valuearray) {
716			foreach ($valuearray as $key => $value) {
717				if (($tag_key == 'ATTACHED_PICTURE') && is_array($value)) {
718					continue; // handled separately in write.metaflac.php
719				} else {
720					str_replace("\r", "\n", $value);
721					if (strstr($value, "\n")) {
722						unset($tag_data_vorbiscomment[$tag_key][$key]);
723						$multilineexploded = explode("\n", $value);
724						foreach ($multilineexploded as $newcomment) {
725							if (strlen(trim($newcomment)) > 0) {
726								$tag_data_vorbiscomment[$tag_key][] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $newcomment);
727							}
728						}
729					} elseif (is_string($value) || is_numeric($value)) {
730						$tag_data_vorbiscomment[$tag_key][$key] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $value);
731					} else {
732						$this->warnings[] = '$data['.$tag_key.']['.$key.'] is not a string value - all of $data['.$tag_key.'] NOT written to VorbisComment tag';
733						unset($tag_data_vorbiscomment[$tag_key]);
734						break;
735					}
736				}
737			}
738		}
739		$this->MergeExistingTagData('vorbiscomment', $tag_data_vorbiscomment);
740		return $tag_data_vorbiscomment;
741	}
742
743	/**
744	 * @return array
745	 */
746	public function FormatDataForMetaFLAC() {
747		// FLAC & OggFLAC use VorbisComments same as OggVorbis
748		// but require metaflac to do the writing rather than vorbiscomment
749		return $this->FormatDataForVorbisComment();
750	}
751
752	/**
753	 * @return array
754	 */
755	public function FormatDataForReal() {
756		$tag_data_real['title']     = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['TITLE']    ) ? $this->tag_data['TITLE']     : array())));
757		$tag_data_real['artist']    = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['ARTIST']   ) ? $this->tag_data['ARTIST']    : array())));
758		$tag_data_real['copyright'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['COPYRIGHT']) ? $this->tag_data['COPYRIGHT'] : array())));
759		$tag_data_real['comment']   = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['COMMENT']  ) ? $this->tag_data['COMMENT']   : array())));
760
761		$this->MergeExistingTagData('real', $tag_data_real);
762		return $tag_data_real;
763	}
764
765}
766