Fixed issue with large title lable
[someplayer] / src / taglib / mp4 / mp4tag.cpp
1 /**************************************************************************
2     copyright            : (C) 2007 by Lukáš Lalinský
3     email                : lalinsky@gmail.com
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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
19  *   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 #ifdef HAVE_CONFIG_H
27 #include <config.h>
28 #endif
29
30 #ifdef WITH_MP4
31
32 #include <tdebug.h>
33 #include <tstring.h>
34 #include "mp4atom.h"
35 #include "mp4tag.h"
36 #include "id3v1genres.h"
37
38 using namespace TagLib;
39
40 class MP4::Tag::TagPrivate
41 {
42 public:
43   TagPrivate() : file(0), atoms(0) {}
44   ~TagPrivate() {}
45   TagLib::File *file;
46   Atoms *atoms;
47   ItemListMap items;
48 };
49
50 MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms)
51 {
52   d = new TagPrivate;
53   d->file = file;
54   d->atoms = atoms;
55
56   MP4::Atom *ilst = atoms->find("moov", "udta", "meta", "ilst");
57   if(!ilst) {
58     //debug("Atom moov.udta.meta.ilst not found.");
59     return;
60   }
61
62   for(unsigned int i = 0; i < ilst->children.size(); i++) {
63     MP4::Atom *atom = ilst->children[i];
64     file->seek(atom->offset + 8);
65     if(atom->name == "----") {
66       parseFreeForm(atom, file);
67     }
68     else if(atom->name == "trkn" || atom->name == "disk") {
69       parseIntPair(atom, file);
70     }
71     else if(atom->name == "cpil" || atom->name == "pgap" || atom->name == "pcst") {
72       parseBool(atom, file);
73     }
74     else if(atom->name == "tmpo") {
75       parseInt(atom, file);
76     }
77     else if(atom->name == "gnre") {
78       parseGnre(atom, file);
79     }
80     else if(atom->name == "covr") {
81       parseCovr(atom, file);
82     }
83     else {
84       parseText(atom, file);
85     }
86   }
87 }
88
89 MP4::Tag::~Tag()
90 {
91   delete d;
92 }
93
94 ByteVectorList
95 MP4::Tag::parseData(MP4::Atom *atom, TagLib::File *file, int expectedFlags, bool freeForm)
96 {
97   ByteVectorList result;
98   ByteVector data = file->readBlock(atom->length - 8);
99   int i = 0;
100   unsigned int pos = 0;
101   while(pos < data.size()) {
102     int length = data.mid(pos, 4).toUInt();
103     ByteVector name = data.mid(pos + 4, 4);
104     int flags = data.mid(pos + 8, 4).toUInt();
105     if(freeForm && i < 2) {
106       if(i == 0 && name != "mean") {
107         debug("MP4: Unexpected atom \"" + name + "\", expecting \"mean\"");
108         return result;
109       }
110       else if(i == 1 && name != "name") {
111         debug("MP4: Unexpected atom \"" + name + "\", expecting \"name\"");
112         return result;
113       }
114       result.append(data.mid(pos + 12, length - 12));
115     }
116     else {
117       if(name != "data") {
118         debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\"");
119         return result;
120       }
121       if(expectedFlags == -1 || flags == expectedFlags) {
122         result.append(data.mid(pos + 16, length - 16));
123       }
124     }
125     pos += length;
126     i++;
127   }
128   return result;
129 }
130
131 void
132 MP4::Tag::parseInt(MP4::Atom *atom, TagLib::File *file)
133 {
134   ByteVectorList data = parseData(atom, file);
135   if(data.size()) {
136     d->items.insert(atom->name, (int)data[0].toShort());
137   }
138 }
139
140 void
141 MP4::Tag::parseGnre(MP4::Atom *atom, TagLib::File *file)
142 {
143   ByteVectorList data = parseData(atom, file);
144   if(data.size()) {
145     int idx = (int)data[0].toShort();
146     if(!d->items.contains("\251gen") && idx > 0) {
147       d->items.insert("\251gen", StringList(ID3v1::genre(idx - 1)));
148     }
149   }
150 }
151
152 void
153 MP4::Tag::parseIntPair(MP4::Atom *atom, TagLib::File *file)
154 {
155   ByteVectorList data = parseData(atom, file);
156   if(data.size()) {
157     int a = data[0].mid(2, 2).toShort();
158     int b = data[0].mid(4, 2).toShort();
159     d->items.insert(atom->name, MP4::Item(a, b));
160   }
161 }
162
163 void
164 MP4::Tag::parseBool(MP4::Atom *atom, TagLib::File *file)
165 {
166   ByteVectorList data = parseData(atom, file);
167   if(data.size()) {
168     bool value = data[0].size() ? data[0][0] != '\0' : false;
169     d->items.insert(atom->name, value);
170   }
171 }
172
173 void
174 MP4::Tag::parseText(MP4::Atom *atom, TagLib::File *file, int expectedFlags)
175 {
176   ByteVectorList data = parseData(atom, file, expectedFlags);
177   if(data.size()) {
178     StringList value;
179     for(unsigned int i = 0; i < data.size(); i++) {
180       value.append(String(data[i], String::UTF8));
181     }
182     d->items.insert(atom->name, value);
183   }
184 }
185
186 void
187 MP4::Tag::parseFreeForm(MP4::Atom *atom, TagLib::File *file)
188 {
189   ByteVectorList data = parseData(atom, file, 1, true);
190   if(data.size() > 2) {
191     StringList value;
192     for(unsigned int i = 2; i < data.size(); i++) {
193       value.append(String(data[i], String::UTF8));
194     }
195     String name = "----:" + data[0] + ':' + data[1];
196     d->items.insert(name, value);
197   }
198 }
199
200 void
201 MP4::Tag::parseCovr(MP4::Atom *atom, TagLib::File *file)
202 {
203   MP4::CoverArtList value;
204   ByteVector data = file->readBlock(atom->length - 8);
205   unsigned int pos = 0;
206   while(pos < data.size()) {
207     int length = data.mid(pos, 4).toUInt();
208     ByteVector name = data.mid(pos + 4, 4);
209     int flags = data.mid(pos + 8, 4).toUInt();
210     if(name != "data") {
211       debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\"");
212       break;
213     }
214     if(flags == MP4::CoverArt::PNG || flags == MP4::CoverArt::JPEG) {
215       value.append(MP4::CoverArt(MP4::CoverArt::Format(flags),
216                                  data.mid(pos + 16, length - 16)));
217     }
218     pos += length;
219   }
220   if(value.size() > 0)
221     d->items.insert(atom->name, value);
222 }
223
224 ByteVector
225 MP4::Tag::padIlst(const ByteVector &data, int length)
226 {
227   if (length == -1) {
228     length = ((data.size() + 1023) & ~1023) - data.size();
229   }
230   return renderAtom("free", ByteVector(length, '\1'));
231 }
232
233 ByteVector
234 MP4::Tag::renderAtom(const ByteVector &name, const ByteVector &data)
235 {
236   return ByteVector::fromUInt(data.size() + 8) + name + data;
237 }
238
239 ByteVector
240 MP4::Tag::renderData(const ByteVector &name, int flags, const ByteVectorList &data)
241 {
242   ByteVector result;
243   for(unsigned int i = 0; i < data.size(); i++) {
244     result.append(renderAtom("data", ByteVector::fromUInt(flags) + ByteVector(4, '\0') + data[i]));
245   }
246   return renderAtom(name, result);
247 }
248
249 ByteVector
250 MP4::Tag::renderBool(const ByteVector &name, MP4::Item &item)
251 {
252   ByteVectorList data;
253   data.append(ByteVector(1, item.toBool() ? '\1' : '\0'));
254   return renderData(name, 0x15, data);
255 }
256
257 ByteVector
258 MP4::Tag::renderInt(const ByteVector &name, MP4::Item &item)
259 {
260   ByteVectorList data;
261   data.append(ByteVector::fromShort(item.toInt()));
262   return renderData(name, 0x15, data);
263 }
264
265 ByteVector
266 MP4::Tag::renderIntPair(const ByteVector &name, MP4::Item &item)
267 {
268   ByteVectorList data;
269   data.append(ByteVector(2, '\0') +
270               ByteVector::fromShort(item.toIntPair().first) +
271               ByteVector::fromShort(item.toIntPair().second) +
272               ByteVector(2, '\0'));
273   return renderData(name, 0x00, data);
274 }
275
276 ByteVector
277 MP4::Tag::renderIntPairNoTrailing(const ByteVector &name, MP4::Item &item)
278 {
279   ByteVectorList data;
280   data.append(ByteVector(2, '\0') +
281               ByteVector::fromShort(item.toIntPair().first) +
282               ByteVector::fromShort(item.toIntPair().second));
283   return renderData(name, 0x00, data);
284 }
285
286 ByteVector
287 MP4::Tag::renderText(const ByteVector &name, MP4::Item &item, int flags)
288 {
289   ByteVectorList data;
290   StringList value = item.toStringList();
291   for(unsigned int i = 0; i < value.size(); i++) {
292     data.append(value[i].data(String::UTF8));
293   }
294   return renderData(name, flags, data);
295 }
296
297 ByteVector
298 MP4::Tag::renderCovr(const ByteVector &name, MP4::Item &item)
299 {
300   ByteVector data;
301   MP4::CoverArtList value = item.toCoverArtList();
302   for(unsigned int i = 0; i < value.size(); i++) {
303     data.append(renderAtom("data", ByteVector::fromUInt(value[i].format()) +
304                                    ByteVector(4, '\0') + value[i].data()));
305   }
306   return renderAtom(name, data);
307 }
308
309 ByteVector
310 MP4::Tag::renderFreeForm(const String &name, MP4::Item &item)
311 {
312   StringList header = StringList::split(name, ":");
313   if (header.size() != 3) {
314     debug("MP4: Invalid free-form item name \"" + name + "\"");
315     return ByteVector::null;
316   }
317   ByteVector data;
318   data.append(renderAtom("mean", ByteVector::fromUInt(0) + header[1].data(String::UTF8)));
319   data.append(renderAtom("name", ByteVector::fromUInt(0) + header[2].data(String::UTF8)));
320   StringList value = item.toStringList();
321   for(unsigned int i = 0; i < value.size(); i++) {
322     data.append(renderAtom("data", ByteVector::fromUInt(1) + ByteVector(4, '\0') + value[i].data(String::UTF8)));
323   }
324   return renderAtom("----", data);
325 }
326
327 bool
328 MP4::Tag::save()
329 {
330   ByteVector data;
331   for(MP4::ItemListMap::Iterator i = d->items.begin(); i != d->items.end(); i++) {
332     const String name = i->first;
333     if(name.startsWith("----")) {
334       data.append(renderFreeForm(name, i->second));
335     }
336     else if(name == "trkn") {
337       data.append(renderIntPair(name.data(String::Latin1), i->second));
338     }
339     else if(name == "disk") {
340       data.append(renderIntPairNoTrailing(name.data(String::Latin1), i->second));
341     }
342     else if(name == "cpil" || name == "pgap" || name == "pcst") {
343       data.append(renderBool(name.data(String::Latin1), i->second));
344     }
345     else if(name == "tmpo") {
346       data.append(renderInt(name.data(String::Latin1), i->second));
347     }
348     else if(name == "covr") {
349       data.append(renderCovr(name.data(String::Latin1), i->second));
350     }
351     else if(name.size() == 4){
352       data.append(renderText(name.data(String::Latin1), i->second));
353     }
354     else {
355       debug("MP4: Unknown item name \"" + name + "\"");
356     }
357   }
358   data = renderAtom("ilst", data);
359
360   AtomList path = d->atoms->path("moov", "udta", "meta", "ilst");
361   if(path.size() == 4) {
362     saveExisting(data, path);
363   }
364   else {
365     saveNew(data);
366   }
367
368   return true;
369 }
370
371 void
372 MP4::Tag::updateParents(AtomList &path, long delta, int ignore)
373 {
374   for(unsigned int i = 0; i < path.size() - ignore; i++) {
375     d->file->seek(path[i]->offset);
376     long size = d->file->readBlock(4).toUInt();
377     // 64-bit
378     if (size == 1) {
379       d->file->seek(4, File::Current); // Skip name
380       long long longSize = d->file->readBlock(8).toLongLong();
381       // Seek the offset of the 64-bit size
382       d->file->seek(path[i]->offset + 8);
383       d->file->writeBlock(ByteVector::fromLongLong(longSize + delta));
384     }
385     // 32-bit
386     else {
387       d->file->seek(path[i]->offset);
388       d->file->writeBlock(ByteVector::fromUInt(size + delta));
389     }
390   }
391 }
392
393 void
394 MP4::Tag::updateOffsets(long delta, long offset)
395 {
396   MP4::Atom *moov = d->atoms->find("moov");
397   if(moov) {
398     MP4::AtomList stco = moov->findall("stco", true);
399     for(unsigned int i = 0; i < stco.size(); i++) {
400       MP4::Atom *atom = stco[i];
401       if(atom->offset > offset) {
402         atom->offset += delta;
403       }
404       d->file->seek(atom->offset + 12);
405       ByteVector data = d->file->readBlock(atom->length - 12);
406       unsigned int count = data.mid(0, 4).toUInt();
407       d->file->seek(atom->offset + 16);
408       int pos = 4;
409       while(count--) {
410         long o = data.mid(pos, 4).toUInt();
411         if(o > offset) {
412           o += delta;
413         }
414         d->file->writeBlock(ByteVector::fromUInt(o));
415         pos += 4;
416       }
417     }
418
419     MP4::AtomList co64 = moov->findall("co64", true);
420     for(unsigned int i = 0; i < co64.size(); i++) {
421       MP4::Atom *atom = co64[i];
422       if(atom->offset > offset) {
423         atom->offset += delta;
424       }
425       d->file->seek(atom->offset + 12);
426       ByteVector data = d->file->readBlock(atom->length - 12);
427       unsigned int count = data.mid(0, 4).toUInt();
428       d->file->seek(atom->offset + 16);
429       int pos = 4;
430       while(count--) {
431         long long o = data.mid(pos, 8).toLongLong();
432         if(o > offset) {
433           o += delta;
434         }
435         d->file->writeBlock(ByteVector::fromLongLong(o));
436         pos += 8;
437       }
438     }
439   }
440
441   MP4::Atom *moof = d->atoms->find("moof");
442   if(moof) {
443     MP4::AtomList tfhd = moof->findall("tfhd", true);
444     for(unsigned int i = 0; i < tfhd.size(); i++) {
445       MP4::Atom *atom = tfhd[i];
446       if(atom->offset > offset) {
447         atom->offset += delta;
448       }
449       d->file->seek(atom->offset + 9);
450       ByteVector data = d->file->readBlock(atom->offset - 9);
451       unsigned int flags = (ByteVector(1, '\0') + data.mid(0, 3)).toUInt();
452       if(flags & 1) {
453         long long o = data.mid(7, 8).toLongLong();
454         if(o > offset) {
455           o += delta;
456         }
457         d->file->seek(atom->offset + 16);
458         d->file->writeBlock(ByteVector::fromLongLong(o));
459       }
460     }
461   }
462 }
463
464 void
465 MP4::Tag::saveNew(ByteVector &data)
466 {
467   data = renderAtom("meta", TagLib::ByteVector(4, '\0') +
468                     renderAtom("hdlr", TagLib::ByteVector(8, '\0') + TagLib::ByteVector("mdirappl") + TagLib::ByteVector(9, '\0')) +
469                     data + padIlst(data));
470
471   AtomList path = d->atoms->path("moov", "udta");
472   if(path.size() != 2) {
473     path = d->atoms->path("moov");
474     data = renderAtom("udta", data);
475   }
476
477   long offset = path[path.size() - 1]->offset + 8;
478   d->file->insert(data, offset, 0);
479
480   updateParents(path, data.size());
481   updateOffsets(data.size(), offset);
482 }
483
484 void
485 MP4::Tag::saveExisting(ByteVector &data, AtomList &path)
486 {
487   MP4::Atom *ilst = path[path.size() - 1];
488   long offset = ilst->offset;
489   long length = ilst->length;
490
491   MP4::Atom *meta = path[path.size() - 2];
492   AtomList::Iterator index = meta->children.find(ilst);
493
494   // check if there is an atom before 'ilst', and possibly use it as padding
495   if(index != meta->children.begin()) {
496     AtomList::Iterator prevIndex = index;
497     prevIndex--;
498     MP4::Atom *prev = *prevIndex;
499     if(prev->name == "free") {
500       offset = prev->offset;
501       length += prev->length;
502     }
503   }
504   // check if there is an atom after 'ilst', and possibly use it as padding
505   AtomList::Iterator nextIndex = index;
506   nextIndex++;
507   if(nextIndex != meta->children.end()) {
508     MP4::Atom *next = *nextIndex;
509     if(next->name == "free") {
510       length += next->length;
511     }
512   }
513
514   long delta = data.size() - length;
515   if(delta > 0 || (delta < 0 && delta > -8)) {
516     data.append(padIlst(data));
517     delta = data.size() - length;
518   }
519   else if(delta < 0) {
520     data.append(padIlst(data, -delta - 8));
521     delta = 0;
522   }
523
524   d->file->insert(data, offset, length);
525
526   if(delta) {
527     updateParents(path, delta, 1);
528     updateOffsets(delta, offset);
529   }
530 }
531
532 String
533 MP4::Tag::title() const
534 {
535   if(d->items.contains("\251nam"))
536     return d->items["\251nam"].toStringList().toString(", ");
537   return String::null;
538 }
539
540 String
541 MP4::Tag::artist() const
542 {
543   if(d->items.contains("\251ART"))
544     return d->items["\251ART"].toStringList().toString(", ");
545   return String::null;
546 }
547
548 String
549 MP4::Tag::album() const
550 {
551   if(d->items.contains("\251alb"))
552     return d->items["\251alb"].toStringList().toString(", ");
553   return String::null;
554 }
555
556 String
557 MP4::Tag::comment() const
558 {
559   if(d->items.contains("\251cmt"))
560     return d->items["\251cmt"].toStringList().toString(", ");
561   return String::null;
562 }
563
564 String
565 MP4::Tag::genre() const
566 {
567   if(d->items.contains("\251gen"))
568     return d->items["\251gen"].toStringList().toString(", ");
569   return String::null;
570 }
571
572 unsigned int
573 MP4::Tag::year() const
574 {
575   if(d->items.contains("\251day"))
576     return d->items["\251day"].toStringList().toString().toInt();
577   return 0;
578 }
579
580 unsigned int
581 MP4::Tag::track() const
582 {
583   if(d->items.contains("trkn"))
584     return d->items["trkn"].toIntPair().first;
585   return 0;
586 }
587
588 void
589 MP4::Tag::setTitle(const String &value)
590 {
591   d->items["\251nam"] = StringList(value);
592 }
593
594 void
595 MP4::Tag::setArtist(const String &value)
596 {
597   d->items["\251ART"] = StringList(value);
598 }
599
600 void
601 MP4::Tag::setAlbum(const String &value)
602 {
603   d->items["\251alb"] = StringList(value);
604 }
605
606 void
607 MP4::Tag::setComment(const String &value)
608 {
609   d->items["\251cmt"] = StringList(value);
610 }
611
612 void
613 MP4::Tag::setGenre(const String &value)
614 {
615   d->items["\251gen"] = StringList(value);
616 }
617
618 void
619 MP4::Tag::setYear(uint value)
620 {
621   d->items["\251day"] = StringList(String::number(value));
622 }
623
624 void
625 MP4::Tag::setTrack(uint value)
626 {
627   d->items["trkn"] = MP4::Item(value, 0);
628 }
629
630 MP4::ItemListMap &
631 MP4::Tag::itemListMap()
632 {
633   return d->items;
634 }
635
636 #endif