1 /***************************************************************************
2 copyright : (C) 2002 - 2008 by Scott Wheeler
3 email : wheeler@kde.org
4 ***************************************************************************/
5
6 /***************************************************************************
7 * This library is free software; you can redistribute it and/or modify *
8 * it under the terms of the GNU Lesser General Public License version *
9 * 2.1 as published by the Free Software Foundation. *
10 * *
11 * This library is distributed in the hope that it will be useful, but *
12 * WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
14 * Lesser General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU Lesser General Public *
17 * License along with this library; if not, write to the Free Software *
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
19 * 02110-1301 USA *
20 * *
21 * Alternatively, this file is available under the Mozilla Public *
22 * License Version 1.1. You may obtain a copy of the License at *
23 * http://www.mozilla.org/MPL/ *
24 ***************************************************************************/
25
26 #include <tbytevector.h>
27 #include <tdebug.h>
28
29 #include <flacpicture.h>
30 #include <xiphcomment.h>
31 #include <tpropertymap.h>
32
33 using namespace TagLib;
34
35 namespace
36 {
37 typedef Ogg::FieldListMap::Iterator FieldIterator;
38 typedef Ogg::FieldListMap::ConstIterator FieldConstIterator;
39
40 typedef List<FLAC::Picture *> PictureList;
41 typedef PictureList::Iterator PictureIterator;
42 typedef PictureList::Iterator PictureConstIterator;
43 }
44
45 class Ogg::XiphComment::XiphCommentPrivate
46 {
47 public:
XiphCommentPrivate()48 XiphCommentPrivate()
49 {
50 pictureList.setAutoDelete(true);
51 }
52
53 FieldListMap fieldListMap;
54 String vendorID;
55 String commentField;
56 PictureList pictureList;
57 };
58
59 ////////////////////////////////////////////////////////////////////////////////
60 // public members
61 ////////////////////////////////////////////////////////////////////////////////
62
XiphComment()63 Ogg::XiphComment::XiphComment() :
64 TagLib::Tag(),
65 d(new XiphCommentPrivate())
66 {
67 }
68
XiphComment(const ByteVector & data)69 Ogg::XiphComment::XiphComment(const ByteVector &data) :
70 TagLib::Tag(),
71 d(new XiphCommentPrivate())
72 {
73 parse(data);
74 }
75
~XiphComment()76 Ogg::XiphComment::~XiphComment()
77 {
78 delete d;
79 }
80
title() const81 String Ogg::XiphComment::title() const
82 {
83 if(d->fieldListMap["TITLE"].isEmpty())
84 return String();
85 return d->fieldListMap["TITLE"].toString();
86 }
87
artist() const88 String Ogg::XiphComment::artist() const
89 {
90 if(d->fieldListMap["ARTIST"].isEmpty())
91 return String();
92 return d->fieldListMap["ARTIST"].toString();
93 }
94
album() const95 String Ogg::XiphComment::album() const
96 {
97 if(d->fieldListMap["ALBUM"].isEmpty())
98 return String();
99 return d->fieldListMap["ALBUM"].toString();
100 }
101
comment() const102 String Ogg::XiphComment::comment() const
103 {
104 if(!d->fieldListMap["DESCRIPTION"].isEmpty()) {
105 d->commentField = "DESCRIPTION";
106 return d->fieldListMap["DESCRIPTION"].toString();
107 }
108
109 if(!d->fieldListMap["COMMENT"].isEmpty()) {
110 d->commentField = "COMMENT";
111 return d->fieldListMap["COMMENT"].toString();
112 }
113
114 return String();
115 }
116
genre() const117 String Ogg::XiphComment::genre() const
118 {
119 if(d->fieldListMap["GENRE"].isEmpty())
120 return String();
121 return d->fieldListMap["GENRE"].toString();
122 }
123
year() const124 unsigned int Ogg::XiphComment::year() const
125 {
126 if(!d->fieldListMap["DATE"].isEmpty())
127 return d->fieldListMap["DATE"].front().toInt();
128 if(!d->fieldListMap["YEAR"].isEmpty())
129 return d->fieldListMap["YEAR"].front().toInt();
130 return 0;
131 }
132
track() const133 unsigned int Ogg::XiphComment::track() const
134 {
135 if(!d->fieldListMap["TRACKNUMBER"].isEmpty())
136 return d->fieldListMap["TRACKNUMBER"].front().toInt();
137 if(!d->fieldListMap["TRACKNUM"].isEmpty())
138 return d->fieldListMap["TRACKNUM"].front().toInt();
139 return 0;
140 }
141
setTitle(const String & s)142 void Ogg::XiphComment::setTitle(const String &s)
143 {
144 addField("TITLE", s);
145 }
146
setArtist(const String & s)147 void Ogg::XiphComment::setArtist(const String &s)
148 {
149 addField("ARTIST", s);
150 }
151
setAlbum(const String & s)152 void Ogg::XiphComment::setAlbum(const String &s)
153 {
154 addField("ALBUM", s);
155 }
156
setComment(const String & s)157 void Ogg::XiphComment::setComment(const String &s)
158 {
159 if(d->commentField.isEmpty()) {
160 if(!d->fieldListMap["DESCRIPTION"].isEmpty())
161 d->commentField = "DESCRIPTION";
162 else
163 d->commentField = "COMMENT";
164 }
165
166 addField(d->commentField, s);
167 }
168
setGenre(const String & s)169 void Ogg::XiphComment::setGenre(const String &s)
170 {
171 addField("GENRE", s);
172 }
173
setYear(unsigned int i)174 void Ogg::XiphComment::setYear(unsigned int i)
175 {
176 removeFields("YEAR");
177 if(i == 0)
178 removeFields("DATE");
179 else
180 addField("DATE", String::number(i));
181 }
182
setTrack(unsigned int i)183 void Ogg::XiphComment::setTrack(unsigned int i)
184 {
185 removeFields("TRACKNUM");
186 if(i == 0)
187 removeFields("TRACKNUMBER");
188 else
189 addField("TRACKNUMBER", String::number(i));
190 }
191
isEmpty() const192 bool Ogg::XiphComment::isEmpty() const
193 {
194 for(FieldConstIterator it = d->fieldListMap.begin(); it != d->fieldListMap.end(); ++it) {
195 if(!(*it).second.isEmpty())
196 return false;
197 }
198
199 return true;
200 }
201
fieldCount() const202 unsigned int Ogg::XiphComment::fieldCount() const
203 {
204 unsigned int count = 0;
205
206 for(FieldConstIterator it = d->fieldListMap.begin(); it != d->fieldListMap.end(); ++it)
207 count += (*it).second.size();
208
209 count += d->pictureList.size();
210
211 return count;
212 }
213
fieldListMap() const214 const Ogg::FieldListMap &Ogg::XiphComment::fieldListMap() const
215 {
216 return d->fieldListMap;
217 }
218
properties() const219 PropertyMap Ogg::XiphComment::properties() const
220 {
221 return d->fieldListMap;
222 }
223
setProperties(const PropertyMap & properties)224 PropertyMap Ogg::XiphComment::setProperties(const PropertyMap &properties)
225 {
226 // check which keys are to be deleted
227 StringList toRemove;
228 for(FieldConstIterator it = d->fieldListMap.begin(); it != d->fieldListMap.end(); ++it)
229 if (!properties.contains(it->first))
230 toRemove.append(it->first);
231
232 for(StringList::ConstIterator it = toRemove.begin(); it != toRemove.end(); ++it)
233 removeFields(*it);
234
235 // now go through keys in \a properties and check that the values match those in the xiph comment
236 PropertyMap invalid;
237 PropertyMap::ConstIterator it = properties.begin();
238 for(; it != properties.end(); ++it)
239 {
240 if(!checkKey(it->first))
241 invalid.insert(it->first, it->second);
242 else if(!d->fieldListMap.contains(it->first) || !(it->second == d->fieldListMap[it->first])) {
243 const StringList &sl = it->second;
244 if(sl.isEmpty())
245 // zero size string list -> remove the tag with all values
246 removeFields(it->first);
247 else {
248 // replace all strings in the list for the tag
249 StringList::ConstIterator valueIterator = sl.begin();
250 addField(it->first, *valueIterator, true);
251 ++valueIterator;
252 for(; valueIterator != sl.end(); ++valueIterator)
253 addField(it->first, *valueIterator, false);
254 }
255 }
256 }
257 return invalid;
258 }
259
checkKey(const String & key)260 bool Ogg::XiphComment::checkKey(const String &key)
261 {
262 if(key.size() < 1)
263 return false;
264
265 // A key may consist of ASCII 0x20 through 0x7D, 0x3D ('=') excluded.
266
267 for(String::ConstIterator it = key.begin(); it != key.end(); it++) {
268 if(*it < 0x20 || *it > 0x7D || *it == 0x3D)
269 return false;
270 }
271
272 return true;
273 }
274
vendorID() const275 String Ogg::XiphComment::vendorID() const
276 {
277 return d->vendorID;
278 }
279
addField(const String & key,const String & value,bool replace)280 void Ogg::XiphComment::addField(const String &key, const String &value, bool replace)
281 {
282 if(!checkKey(key)) {
283 debug("Ogg::XiphComment::addField() - Invalid key. Field not added.");
284 return;
285 }
286
287 const String upperKey = key.upper();
288
289 if(replace)
290 removeFields(upperKey);
291
292 if(!key.isEmpty() && !value.isEmpty())
293 d->fieldListMap[upperKey].append(value);
294 }
295
removeField(const String & key,const String & value)296 void Ogg::XiphComment::removeField(const String &key, const String &value)
297 {
298 if(!value.isNull())
299 removeFields(key, value);
300 else
301 removeFields(key);
302 }
303
removeFields(const String & key)304 void Ogg::XiphComment::removeFields(const String &key)
305 {
306 d->fieldListMap.erase(key.upper());
307 }
308
removeFields(const String & key,const String & value)309 void Ogg::XiphComment::removeFields(const String &key, const String &value)
310 {
311 StringList &fields = d->fieldListMap[key.upper()];
312 for(StringList::Iterator it = fields.begin(); it != fields.end(); ) {
313 if(*it == value)
314 it = fields.erase(it);
315 else
316 ++it;
317 }
318 }
319
removeAllFields()320 void Ogg::XiphComment::removeAllFields()
321 {
322 d->fieldListMap.clear();
323 }
324
contains(const String & key) const325 bool Ogg::XiphComment::contains(const String &key) const
326 {
327 return !d->fieldListMap[key.upper()].isEmpty();
328 }
329
removePicture(FLAC::Picture * picture,bool del)330 void Ogg::XiphComment::removePicture(FLAC::Picture *picture, bool del)
331 {
332 PictureIterator it = d->pictureList.find(picture);
333 if(it != d->pictureList.end())
334 d->pictureList.erase(it);
335
336 if(del)
337 delete picture;
338 }
339
removeAllPictures()340 void Ogg::XiphComment::removeAllPictures()
341 {
342 d->pictureList.clear();
343 }
344
addPicture(FLAC::Picture * picture)345 void Ogg::XiphComment::addPicture(FLAC::Picture * picture)
346 {
347 d->pictureList.append(picture);
348 }
349
pictureList()350 List<FLAC::Picture *> Ogg::XiphComment::pictureList()
351 {
352 return d->pictureList;
353 }
354
render() const355 ByteVector Ogg::XiphComment::render() const
356 {
357 return render(true);
358 }
359
render(bool addFramingBit) const360 ByteVector Ogg::XiphComment::render(bool addFramingBit) const
361 {
362 ByteVector data;
363
364 // Add the vendor ID length and the vendor ID. It's important to use the
365 // length of the data(String::UTF8) rather than the length of the the string
366 // since this is UTF8 text and there may be more characters in the data than
367 // in the UTF16 string.
368
369 ByteVector vendorData = d->vendorID.data(String::UTF8);
370
371 data.append(ByteVector::fromUInt(vendorData.size(), false));
372 data.append(vendorData);
373
374 // Add the number of fields.
375
376 data.append(ByteVector::fromUInt(fieldCount(), false));
377
378 // Iterate over the the field lists. Our iterator returns a
379 // std::pair<String, StringList> where the first String is the field name and
380 // the StringList is the values associated with that field.
381
382 FieldListMap::ConstIterator it = d->fieldListMap.begin();
383 for(; it != d->fieldListMap.end(); ++it) {
384
385 // And now iterate over the values of the current list.
386
387 String fieldName = (*it).first;
388 StringList values = (*it).second;
389
390 StringList::ConstIterator valuesIt = values.begin();
391 for(; valuesIt != values.end(); ++valuesIt) {
392 ByteVector fieldData = fieldName.data(String::UTF8);
393 fieldData.append('=');
394 fieldData.append((*valuesIt).data(String::UTF8));
395
396 data.append(ByteVector::fromUInt(fieldData.size(), false));
397 data.append(fieldData);
398 }
399 }
400
401 for(PictureConstIterator it = d->pictureList.begin(); it != d->pictureList.end(); ++it) {
402 ByteVector picture = (*it)->render().toBase64();
403 data.append(ByteVector::fromUInt(picture.size() + 23, false));
404 data.append("METADATA_BLOCK_PICTURE=");
405 data.append(picture);
406 }
407
408 // Append the "framing bit".
409
410 if(addFramingBit)
411 data.append(char(1));
412
413 return data;
414 }
415
416 ////////////////////////////////////////////////////////////////////////////////
417 // protected members
418 ////////////////////////////////////////////////////////////////////////////////
419
parse(const ByteVector & data)420 void Ogg::XiphComment::parse(const ByteVector &data)
421 {
422 // The first thing in the comment data is the vendor ID length, followed by a
423 // UTF8 string with the vendor ID.
424
425 unsigned int pos = 0;
426
427 const unsigned int vendorLength = data.toUInt(0, false);
428 pos += 4;
429
430 d->vendorID = String(data.mid(pos, vendorLength), String::UTF8);
431 pos += vendorLength;
432
433 // Next the number of fields in the comment vector.
434
435 const unsigned int commentFields = data.toUInt(pos, false);
436 pos += 4;
437
438 if(commentFields > (data.size() - 8) / 4) {
439 return;
440 }
441
442 for(unsigned int i = 0; i < commentFields; i++) {
443
444 // Each comment field is in the format "KEY=value" in a UTF8 string and has
445 // 4 bytes before the text starts that gives the length.
446
447 const unsigned int commentLength = data.toUInt(pos, false);
448 pos += 4;
449
450 const ByteVector entry = data.mid(pos, commentLength);
451 pos += commentLength;
452
453 // Don't go past data end
454
455 if(pos > data.size())
456 break;
457
458 // Check for field separator
459
460 const int sep = entry.find('=');
461 if(sep < 1) {
462 debug("Ogg::XiphComment::parse() - Discarding a field. Separator not found.");
463 continue;
464 }
465
466 // Parse the key
467
468 const String key = String(entry.mid(0, sep), String::UTF8).upper();
469 if(!checkKey(key)) {
470 debug("Ogg::XiphComment::parse() - Discarding a field. Invalid key.");
471 continue;
472 }
473
474 if(key == "METADATA_BLOCK_PICTURE" || key == "COVERART") {
475
476 // Handle Pictures separately
477
478 const ByteVector picturedata = ByteVector::fromBase64(entry.mid(sep + 1));
479 if(picturedata.isEmpty()) {
480 debug("Ogg::XiphComment::parse() - Discarding a field. Invalid base64 data");
481 continue;
482 }
483
484 if(key[0] == L'M') {
485
486 // Decode FLAC Picture
487
488 FLAC::Picture * picture = new FLAC::Picture();
489 if(picture->parse(picturedata)) {
490 d->pictureList.append(picture);
491 }
492 else {
493 delete picture;
494 debug("Ogg::XiphComment::parse() - Failed to decode FLAC Picture block");
495 }
496 }
497 else {
498
499 // Assume it's some type of image file
500
501 FLAC::Picture * picture = new FLAC::Picture();
502 picture->setData(picturedata);
503 picture->setMimeType("image/");
504 picture->setType(FLAC::Picture::Other);
505 d->pictureList.append(picture);
506 }
507 }
508 else {
509
510 // Parse the text
511
512 addField(key, String(entry.mid(sep + 1), String::UTF8), false);
513 }
514 }
515 }
516