OSDN Git Service

QString::fromLocal8Bit() doesn't work as expected, as we explicitly set the CodecForL...
[lamexp/LameXP.git] / src / Model_CueSheet.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2011 LoRd_MuldeR <MuldeR2@GMX.de>
4 //
5 // This program is free software; you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation; either version 2 of the License, or
8 // (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License along
16 // with this program; if not, write to the Free Software Foundation, Inc.,
17 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 //
19 // http://www.gnu.org/licenses/gpl-2.0.txt
20 ///////////////////////////////////////////////////////////////////////////////
21
22 #include "Global.h"
23 #include "Model_CueSheet.h"
24 #include "Genres.h"
25
26 #include <QApplication>
27 #include <QDir>
28 #include <QFileInfo>
29 #include <QFont>
30 #include <QTime>
31 #include <QTextCodec>
32
33 #include <float.h>
34 #include <limits>
35
36 #define UNQUOTE(STR) STR.split("\"",  QString::SkipEmptyParts).first().trimmed()
37
38 ////////////////////////////////////////////////////////////
39 // Helper Classes
40 ////////////////////////////////////////////////////////////
41
42 class CueSheetItem
43 {
44 public:
45         virtual const char* type(void) = 0;
46         virtual bool isValid(void) { return false; }
47 };
48
49 class CueSheetTrack : public CueSheetItem
50 {
51 public:
52         CueSheetTrack(CueSheetFile *parent, int trackNo)
53         :
54                 m_parent(parent),
55                 m_trackNo(trackNo)
56         {
57                 m_startIndex = std::numeric_limits<double>::quiet_NaN();
58                 m_duration = std::numeric_limits<double>::infinity();
59                 m_year = 0;
60         }
61         int trackNo(void) { return m_trackNo; }
62         double startIndex(void) { return m_startIndex; }
63         double duration(void) { return m_duration; }
64         QString title(void) { return m_title; }
65         QString performer(void) { return m_performer; }
66         QString genre(void) { return m_genre; }
67         unsigned int year(void) { return m_year; }
68         CueSheetFile *parent(void) { return m_parent; }
69         void setStartIndex(double startIndex) { m_startIndex = startIndex; }
70         void setDuration(double duration) { m_duration = duration; }
71         void setTitle(const QString &title, bool update = false) { if(!update || (m_title.isEmpty() && !title.isEmpty())) m_title = title; }
72         void setPerformer(const QString &performer, bool update = false) { if(!update || (m_performer.isEmpty() && !performer.isEmpty())) m_performer = performer; }
73         void setGenre(const QString &genre, bool update = false) { if(!update || (m_genre.isEmpty() && !m_genre.isEmpty())) m_genre = genre; }
74         void setYear(const unsigned int year, bool update = false) { if(!update || (year == 0)) m_year = year; }
75         virtual bool isValid(void) { return !(_isnan(m_startIndex) || (m_trackNo < 0)); }
76         virtual const char* type(void) { return "CueSheetTrack"; }
77 private:
78         int m_trackNo;
79         double m_startIndex;
80         double m_duration;
81         QString m_title;
82         QString m_performer;
83         QString m_genre;
84         unsigned int m_year;
85         CueSheetFile *m_parent;
86 };
87
88 class CueSheetFile : public CueSheetItem
89 {
90 public:
91         CueSheetFile(const QString &fileName) : m_fileName(fileName) {}
92         ~CueSheetFile(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
93         QString fileName(void) { return m_fileName; }
94         void addTrack(CueSheetTrack *track) { m_tracks.append(track); }
95         void clearTracks(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
96         CueSheetTrack *track(int index) { return m_tracks.at(index); }
97         int trackCount(void) { return m_tracks.count(); }
98         virtual bool isValid(void) { return m_tracks.count() > 0; }
99         virtual const char* type(void) { return "CueSheetFile"; }
100 private:
101         const QString m_fileName;
102         QList<CueSheetTrack*> m_tracks;
103 };
104
105 ////////////////////////////////////////////////////////////
106 // Constructor & Destructor
107 ////////////////////////////////////////////////////////////
108
109 QMutex CueSheetModel::m_mutex(QMutex::Recursive);
110
111 CueSheetModel::CueSheetModel()
112 :
113         m_fileIcon(":/icons/music.png"),
114         m_trackIcon(":/icons/control_play_blue.png")
115 {
116         int trackNo = 0;
117         m_albumYear = 0;
118         
119         for(int i = 0; i < 5; i++)
120         {
121                 CueSheetFile *currentFile = new CueSheetFile(QString().sprintf("File %02d.wav", i+1));
122                 for(int j = 0; j < 8; j++)
123                 {
124                         CueSheetTrack *currentTrack = new CueSheetTrack(currentFile, trackNo++);
125                         currentTrack->setTitle("ATWA (Air Trees Water Animals)");
126                         currentTrack->setPerformer("System of a Down");
127                         currentFile->addTrack(currentTrack);
128                 }
129                 m_files.append(currentFile);
130         }
131 }
132
133 CueSheetModel::~CueSheetModel(void)
134 {
135         while(!m_files.isEmpty()) delete m_files.takeLast();
136 }
137
138 ////////////////////////////////////////////////////////////
139 // Model Functions
140 ////////////////////////////////////////////////////////////
141
142 QModelIndex CueSheetModel::index(int row, int column, const QModelIndex &parent) const
143 {
144         QMutexLocker lock(&m_mutex);
145         
146         if(!parent.isValid())
147         {
148                 return (row < m_files.count()) ? createIndex(row, column, m_files.at(row)) : QModelIndex();
149         }
150
151         CueSheetItem *parentItem = static_cast<CueSheetItem*>(parent.internalPointer());
152         if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(parentItem))
153         {
154                 return (row < filePtr->trackCount()) ? createIndex(row, column, filePtr->track(row)) : QModelIndex();
155         }
156
157         return QModelIndex();
158 }
159
160 int CueSheetModel::columnCount(const QModelIndex &parent) const
161 {
162         QMutexLocker lock(&m_mutex);
163         return 4;
164 }
165
166 int CueSheetModel::rowCount(const QModelIndex &parent) const
167 {
168         QMutexLocker lock(&m_mutex);
169
170         if(!parent.isValid())
171         {
172                 return m_files.count();
173         }
174
175         CueSheetItem *parentItem = static_cast<CueSheetItem*>(parent.internalPointer());
176         if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(parentItem))
177         {
178                 return filePtr->trackCount();
179         }
180
181         return 0;
182 }
183
184 QModelIndex CueSheetModel::parent(const QModelIndex &child) const
185 {
186         QMutexLocker lock(&m_mutex);
187         
188         if(child.isValid())
189         {
190                 CueSheetItem *childItem = static_cast<CueSheetItem*>(child.internalPointer());
191                 if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(childItem))
192                 {
193                         return createIndex(m_files.indexOf(trackPtr->parent()), 0, trackPtr->parent());
194                 }
195         }
196
197         return QModelIndex();
198 }
199
200 QVariant CueSheetModel::headerData (int section, Qt::Orientation orientation, int role) const
201 {
202         QMutexLocker lock(&m_mutex);
203         
204         if(role == Qt::DisplayRole)
205         {
206                 switch(section)
207                 {
208                 case 0:
209                         return tr("No.");
210                         break;
211                 case 1:
212                         return tr("File / Track");
213                         break;
214                 case 2:
215                         return tr("Index");
216                         break;
217                 case 3:
218                         return tr("Duration");
219                         break;
220                 default:
221                         return QVariant();
222                         break;
223                 }
224         }
225         else
226         {
227                 return QVariant();
228         }
229 }
230
231 QVariant CueSheetModel::data(const QModelIndex &index, int role) const
232 {
233         QMutexLocker lock(&m_mutex);
234
235         if(role == Qt::DisplayRole)
236         {
237                 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
238
239                 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(item))
240                 {
241                         switch(index.column())
242                         {
243                         case 0:
244                                 return tr("File %1").arg(QString().sprintf("%02d", index.row() + 1)).append(" ");
245                                 break;
246                         case 1:
247                                 return QFileInfo(filePtr->fileName()).fileName();
248                                 break;
249                         default:
250                                 return QVariant();
251                                 break;
252                         }
253                 }
254                 else if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
255                 {
256                         switch(index.column())
257                         {
258                         case 0:
259                                 return tr("Track %1").arg(QString().sprintf("%02d", trackPtr->trackNo())).append(" ");
260                                 break;
261                         case 1:
262                                 if(!trackPtr->title().isEmpty() && !trackPtr->performer().isEmpty())
263                                 {
264                                         return QString("%1 - %2").arg(trackPtr->performer(), trackPtr->title());
265                                 }
266                                 else if(!trackPtr->title().isEmpty())
267                                 {
268                                         return QString("%1 - %2").arg(tr("Unknown Artist"), trackPtr->title());
269                                 }
270                                 else if(!trackPtr->performer().isEmpty())
271                                 {
272                                         return QString("%1 - %2").arg(trackPtr->performer(), tr("Unknown Title"));
273                                 }
274                                 else
275                                 {
276                                         return QString("%1 - %2").arg(tr("Unknown Artist"), tr("Unknown Title"));
277                                 }
278                                 break;
279                         case 2:
280                                 return indexToString(trackPtr->startIndex());
281                                 break;
282                         case 3:
283                                 return indexToString(trackPtr->duration());
284                                 break;
285                         default:
286                                 return QVariant();
287                                 break;
288                         }
289                 }
290         }
291         else if(role == Qt::ToolTipRole)
292         {
293                 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
294
295                 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(item))
296                 {
297                         return QDir::toNativeSeparators(filePtr->fileName());
298                 }
299                 else if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
300                 {
301                         return QDir::toNativeSeparators(trackPtr->parent()->fileName());
302                 }
303         }
304         else if(role == Qt::DecorationRole)
305         {
306                 if(index.column() == 0)
307                 {
308                         CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
309
310                         if(dynamic_cast<CueSheetFile*>(item))
311                         {
312                                 return m_fileIcon;
313                         }
314                         else if(dynamic_cast<CueSheetTrack*>(item))
315                         {
316                                 return m_trackIcon;
317                         }
318                 }
319         }
320         else if(role == Qt::FontRole)
321         {
322                 QFont font("Monospace");
323                 font.setStyleHint(QFont::TypeWriter);
324                 if((index.column() == 1))
325                 {
326                         CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
327                         font.setBold(dynamic_cast<CueSheetFile*>(item) != NULL);
328                 }
329                 return font;
330         }
331         else if(role == Qt::ForegroundRole)
332         {
333                 if((index.column() == 1))
334                 {
335                         CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
336                         if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(item))
337                         {
338                                 return (QFileInfo(filePtr->fileName()).size() > 4) ? QColor("mediumblue") : QColor("darkred");
339                         }
340                 }
341                 else if((index.column() == 3))
342                 {
343                         CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
344                         if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
345                         {
346                                 if(trackPtr->duration() == std::numeric_limits<double>::infinity())
347                                 {
348                                         return QColor("dimgrey");
349                                 }
350                         }
351                 }
352         }
353
354         return QVariant();
355 }
356
357 void CueSheetModel::clearData(void)
358 {
359         QMutexLocker lock(&m_mutex);
360         
361         beginResetModel();
362         while(!m_files.isEmpty()) delete m_files.takeLast();
363         endResetModel();
364 }
365
366 ////////////////////////////////////////////////////////////
367 // External API
368 ////////////////////////////////////////////////////////////
369
370 int CueSheetModel::getFileCount(void)
371 {
372         QMutexLocker lock(&m_mutex);
373         return m_files.count();
374 }
375
376 QString CueSheetModel::getFileName(int fileIndex)
377 {
378         QMutexLocker lock(&m_mutex);
379         
380         if(fileIndex < 0 || fileIndex >= m_files.count())
381         {
382                 return QString();
383         }
384
385         return m_files.at(fileIndex)->fileName();
386 }
387
388 int CueSheetModel::getTrackCount(int fileIndex)
389 {
390         QMutexLocker lock(&m_mutex);
391
392         if(fileIndex < 0 || fileIndex >= m_files.count())
393         {
394                 return -1;
395         }
396
397         return m_files.at(fileIndex)->trackCount();
398 }
399
400 int CueSheetModel::getTrackNo(int fileIndex, int trackIndex)
401 {
402         QMutexLocker lock(&m_mutex);
403         
404         if(fileIndex >= 0 && fileIndex < m_files.count())
405         {
406                 CueSheetFile *currentFile = m_files.at(fileIndex);
407                 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
408                 {
409                         return currentFile->track(trackIndex)->trackNo();
410                 }
411         }
412
413         return -1;
414 }
415
416 void CueSheetModel::getTrackIndex(int fileIndex, int trackIndex, double *startIndex, double *duration)
417 {
418         QMutexLocker lock(&m_mutex);
419         
420         *startIndex = std::numeric_limits<double>::quiet_NaN();
421         *duration = std::numeric_limits<double>::quiet_NaN();
422
423         if(fileIndex >= 0 && fileIndex < m_files.count())
424         {
425                 CueSheetFile *currentFile = m_files.at(fileIndex);
426                 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
427                 {
428                         CueSheetTrack *currentTrack = currentFile->track(trackIndex);
429                         *startIndex = currentTrack->startIndex();
430                         *duration = currentTrack->duration();
431                 }
432         }
433 }
434
435 QString CueSheetModel::getTrackPerformer(int fileIndex, int trackIndex)
436 {       
437         QMutexLocker lock(&m_mutex);
438         
439         if(fileIndex >= 0 && fileIndex < m_files.count())
440         {
441                 CueSheetFile *currentFile = m_files.at(fileIndex);
442                 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
443                 {
444                         CueSheetTrack *currentTrack = currentFile->track(trackIndex);
445                         return currentTrack->performer();
446                 }
447         }
448         
449         return QString();
450 }
451
452 QString CueSheetModel::getTrackTitle(int fileIndex, int trackIndex)
453 {
454         QMutexLocker lock(&m_mutex);
455         
456         if(fileIndex >= 0 && fileIndex < m_files.count())
457         {
458                 CueSheetFile *currentFile = m_files.at(fileIndex);
459                 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
460                 {
461                         CueSheetTrack *currentTrack = currentFile->track(trackIndex);
462                         return currentTrack->title();
463                 }
464         }
465         
466         return QString();
467 }
468
469 QString CueSheetModel::getTrackGenre(int fileIndex, int trackIndex)
470 {
471         QMutexLocker lock(&m_mutex);
472         
473         if(fileIndex >= 0 && fileIndex < m_files.count())
474         {
475                 CueSheetFile *currentFile = m_files.at(fileIndex);
476                 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
477                 {
478                         CueSheetTrack *currentTrack = currentFile->track(trackIndex);
479                         return currentTrack->genre();
480                 }
481         }
482         
483         return QString();
484 }
485
486 unsigned int CueSheetModel::getTrackYear(int fileIndex, int trackIndex)
487 {
488         QMutexLocker lock(&m_mutex);
489         
490         if(fileIndex >= 0 && fileIndex < m_files.count())
491         {
492                 CueSheetFile *currentFile = m_files.at(fileIndex);
493                 if(trackIndex >= 0 && trackIndex < currentFile->trackCount())
494                 {
495                         CueSheetTrack *currentTrack = currentFile->track(trackIndex);
496                         return currentTrack->year();
497                 }
498         }
499         
500         return 0;
501 }
502
503 QString CueSheetModel::getAlbumPerformer(void)
504 {
505         QMutexLocker lock(&m_mutex);
506         return m_albumPerformer;
507 }
508
509 QString CueSheetModel::getAlbumTitle(void)
510 {
511         QMutexLocker lock(&m_mutex);
512         return m_albumTitle;
513 }
514
515 QString CueSheetModel::getAlbumGenre(void)
516 {
517         QMutexLocker lock(&m_mutex);
518         return m_albumGenre;
519 }
520
521 unsigned int CueSheetModel::getAlbumYear(void)
522 {
523         QMutexLocker lock(&m_mutex);
524         return m_albumYear;
525 }
526 ////////////////////////////////////////////////////////////
527 // Cue Sheet Parser
528 ////////////////////////////////////////////////////////////
529
530 int CueSheetModel::loadCueSheet(const QString &cueFileName, QCoreApplication *application)
531 {
532         QMutexLocker lock(&m_mutex);
533         
534         QFile cueFile(cueFileName);
535         if(!cueFile.open(QIODevice::ReadOnly))
536         {
537                 return ErrorIOFailure;
538         }
539
540         clearData();
541
542         beginResetModel();
543         int iResult = parseCueFile(cueFile, QDir(QFileInfo(cueFile).canonicalPath()), application);
544         endResetModel();
545
546         return iResult;
547 }
548
549 int CueSheetModel::parseCueFile(QFile &cueFile, const QDir &baseDir, QCoreApplication *application)
550 {
551         cueFile.seek(0);
552         qDebug("\n[Cue Sheet Import]");
553
554         //Reject very large files, as parsing might take until forever
555         if(cueFile.size() >= 10485760i64)
556         {
557                 qWarning("File is very big. Probably not a Cue Sheet. Rejecting...");
558                 return 2;
559         }
560
561         //Get system local Codec
562         const QTextCodec *codec = QTextCodec::codecForName("System");
563
564         //Check for UTF-8 BOM in order to guess encoding
565         bool bUTF8 = false, bForceLatin1 = false;
566         QByteArray bomCheck = cueFile.peek(128);
567         bUTF8 = (!bomCheck.isEmpty()) && bomCheck.contains("\xef\xbb\xbf");
568         qDebug("Encoding is %s.", (bUTF8 ? "UTF-8" : "Local 8-Bit"));
569         bomCheck.clear();
570
571         //Test selected Codepage for decoding errors
572         if(!bUTF8)
573         {
574                 const QString replacementSymbol = QString(QChar(QChar::ReplacementCharacter));
575                 QByteArray data = cueFile.peek(1048576);
576                 if((!data.isEmpty()) && codec->toUnicode(data.constData(), data.size()).contains(replacementSymbol))
577                 {
578                         qWarning("Decoding error using local 8-Bit codepage. Enforcing Latin-1.");
579                         bForceLatin1 = true;
580                 }
581         }
582
583         QRegExp rxFile("^FILE\\s+(\"[^\"]+\"|\\S+)\\s+(\\w+)$", Qt::CaseInsensitive);
584         QRegExp rxTrack("^TRACK\\s+(\\d+)\\s(\\w+)$", Qt::CaseInsensitive);
585         QRegExp rxIndex("^INDEX\\s+(\\d+)\\s+([0-9:]+)$", Qt::CaseInsensitive);
586         QRegExp rxTitle("^TITLE\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive);
587         QRegExp rxPerformer("^PERFORMER\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive);
588         QRegExp rxGenre("^REM\\s+GENRE\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive);
589         QRegExp rxYear("^REM\\s+DATE\\s+(\\d+)$", Qt::CaseInsensitive);
590         
591         bool bPreamble = true;
592         bool bUnsupportedTrack = false;
593
594         CueSheetFile *currentFile = NULL;
595         CueSheetTrack *currentTrack = NULL;
596
597         m_albumTitle.clear();
598         m_albumPerformer.clear();
599         m_albumGenre.clear();
600         m_albumYear = 0;
601
602         //Loop over the Cue Sheet until all lines were processed
603         for(int lines = 0; lines < INT_MAX; lines++)
604         {
605                 if(application)
606                 {
607                         application->processEvents();
608                         if(lines < 128) Sleep(10);
609                 }
610                 
611                 QByteArray lineData = cueFile.readLine();
612                 if(lineData.size() <= 0)
613                 {
614                         qDebug("End of Cue Sheet file.");
615                         break;
616                 }
617
618                 QString line = (bUTF8 ? QString::fromUtf8(lineData.constData(), lineData.size()) : (bForceLatin1 ? QString::fromLatin1(lineData.constData(), lineData.size()) : codec->toUnicode(lineData.constData(), lineData.size()))).trimmed();
619                 
620                 /* --- FILE --- */
621                 if(rxFile.indexIn(line) >= 0)
622                 {
623                         qDebug("%03d File: <%s> <%s>", lines, rxFile.cap(1).toUtf8().constData(), rxFile.cap(2).toUtf8().constData());
624                         if(currentFile)
625                         {
626                                 if(currentTrack)
627                                 {
628                                         if(currentTrack->isValid())
629                                         {
630                                                 currentFile->addTrack(currentTrack);
631                                                 currentTrack = NULL;
632                                         }
633                                         else
634                                         {
635                                                 LAMEXP_DELETE(currentTrack);
636                                         }
637                                 }
638                                 if(currentFile->isValid())
639                                 {
640                                         m_files.append(currentFile);
641                                         currentFile = NULL;
642                                 }
643                                 else
644                                 {
645                                         LAMEXP_DELETE(currentFile);
646                                 }
647                         }
648                         else
649                         {
650                                 LAMEXP_DELETE(currentTrack);
651                         }
652                         if(!rxFile.cap(2).compare("WAVE", Qt::CaseInsensitive) || !rxFile.cap(2).compare("MP3", Qt::CaseInsensitive) || !rxFile.cap(2).compare("AIFF", Qt::CaseInsensitive))
653                         {
654                                 currentFile = new CueSheetFile(baseDir.absoluteFilePath(UNQUOTE(rxFile.cap(1))));
655                                 qDebug("%03d File path: <%s>", lines, currentFile->fileName().toUtf8().constData());
656                         }
657                         else
658                         {
659                                 bUnsupportedTrack = true;
660                                 qWarning("%03d Skipping unsupported file of type '%s'.", lines, rxFile.cap(2).toUtf8().constData());
661                                 currentFile = NULL;
662                         }
663                         bPreamble = false;
664                         currentTrack = NULL;
665                         continue;
666                 }
667                 
668                 /* --- TRACK --- */
669                 if(rxTrack.indexIn(line) >= 0)
670                 {
671                         if(currentFile)
672                         {
673                                 qDebug("%03d   Track: <%s> <%s>", lines, rxTrack.cap(1).toUtf8().constData(), rxTrack.cap(2).toUtf8().constData());
674                                 if(currentTrack)
675                                 {
676                                         if(currentTrack->isValid())
677                                         {
678                                                 currentFile->addTrack(currentTrack);
679                                                 currentTrack = NULL;
680                                         }
681                                         else
682                                         {
683                                                 LAMEXP_DELETE(currentTrack);
684                                         }
685                                 }
686                                 if(!rxTrack.cap(2).compare("AUDIO", Qt::CaseInsensitive))
687                                 {
688                                         currentTrack = new CueSheetTrack(currentFile, rxTrack.cap(1).toInt());
689                                 }
690                                 else
691                                 {
692                                         bUnsupportedTrack = true;
693                                         qWarning("%03d   Skipping unsupported track of type '%s'.", lines, rxTrack.cap(2).toUtf8().constData());
694                                         currentTrack = NULL;
695                                 }
696                         }
697                         else
698                         {
699                                 LAMEXP_DELETE(currentTrack);
700                         }
701                         bPreamble = false;
702                         continue;
703                 }
704                 
705                 /* --- INDEX --- */
706                 if(rxIndex.indexIn(line) >= 0)
707                 {
708                         if(currentFile && currentTrack)
709                         {
710                                 qDebug("%03d     Index: <%s> <%s>", lines, rxIndex.cap(1).toUtf8().constData(), rxIndex.cap(2).toUtf8().constData());
711                                 if(rxIndex.cap(1).toInt() == 1)
712                                 {
713                                         currentTrack->setStartIndex(parseTimeIndex(rxIndex.cap(2)));
714                                 }
715                         }
716                         continue;
717                 }
718
719                 /* --- TITLE --- */
720                 if(rxTitle.indexIn(line) >= 0)
721                 {
722                         if(bPreamble)
723                         {
724                                 m_albumTitle = UNQUOTE(rxTitle.cap(1)).simplified();
725                         }
726                         else if(currentFile && currentTrack)
727                         {
728                                 qDebug("%03d     Title: <%s>", lines, rxTitle.cap(1).toUtf8().constData());
729                                 currentTrack->setTitle(UNQUOTE(rxTitle.cap(1)).simplified());
730                         }
731                         continue;
732                 }
733
734                 /* --- PERFORMER --- */
735                 if(rxPerformer.indexIn(line) >= 0)
736                 {
737                         if(bPreamble)
738                         {
739                                 m_albumPerformer = UNQUOTE(rxPerformer.cap(1)).simplified();
740                         }
741                         else if(currentFile && currentTrack)
742                         {
743                                 qDebug("%03d     Title: <%s>", lines, rxPerformer.cap(1).toUtf8().constData());
744                                 currentTrack->setPerformer(UNQUOTE(rxPerformer.cap(1)).simplified());
745                         }
746                         continue;
747                 }
748
749                 /* --- GENRE --- */
750                 if(rxGenre.indexIn(line) >= 0)
751                 {
752                         if(bPreamble)
753                         {
754                                 QString temp = UNQUOTE(rxGenre.cap(1)).simplified();
755                                 for(int i = 0; g_lamexp_generes[i]; i++)
756                                 {
757                                         if(temp.compare(g_lamexp_generes[i], Qt::CaseInsensitive) == 0)
758                                         {
759                                                 m_albumGenre = QString(g_lamexp_generes[i]);
760                                                 break;
761                                         }
762                                 }
763                         }
764                         else if(currentFile && currentTrack)
765                         {
766                                 qDebug("%03d     Genre: <%s>", lines, rxGenre.cap(1).toUtf8().constData());
767                                 QString temp = UNQUOTE(rxGenre.cap(1).simplified());
768                                 for(int i = 0; g_lamexp_generes[i]; i++)
769                                 {
770                                         if(temp.compare(g_lamexp_generes[i], Qt::CaseInsensitive) == 0)
771                                         {
772                                                 currentTrack->setGenre(QString(g_lamexp_generes[i]));
773                                                 break;
774                                         }
775                                 }
776                         }
777                         continue;
778                 }
779
780                 /* --- YEAR --- */
781                 if(rxYear.indexIn(line) >= 0)
782                 {
783                         if(bPreamble)
784                         {
785                                 bool ok = false;
786                                 unsigned int temp = rxYear.cap(1).toUInt(&ok);
787                                 if(ok) m_albumYear =  temp;
788                         }
789                         else if(currentFile && currentTrack)
790                         {
791                                 qDebug("%03d     Year: <%s>", lines, rxPerformer.cap(1).toUtf8().constData());
792                                 bool ok = false;
793                                 unsigned int temp = rxYear.cap(1).toUInt(&ok);
794                                 if(ok) currentTrack->setYear(temp);
795                         }
796                         continue;
797                 }
798         }
799
800         //Append the very last track/file that is still pending
801         if(currentFile)
802         {
803                 if(currentTrack)
804                 {
805                         if(currentTrack->isValid())
806                         {
807                                 currentFile->addTrack(currentTrack);
808                                 currentTrack = NULL;
809                         }
810                         else
811                         {
812                                 LAMEXP_DELETE(currentTrack);
813                         }
814                 }
815                 if(currentFile->isValid())
816                 {
817                         m_files.append(currentFile);
818                         currentFile = NULL;
819                 }
820                 else
821                 {
822                         LAMEXP_DELETE(currentFile);
823                 }
824         }
825
826         //Finally calculate duration of each track
827         int nFiles = m_files.count();
828         for(int i = 0; i < nFiles; i++)
829         {
830                 if(application)
831                 {
832                         application->processEvents();
833                         Sleep(10);
834                 }
835
836                 CueSheetFile *currentFile = m_files.at(i);
837                 int nTracks = currentFile->trackCount();
838                 if(nTracks > 1)
839                 {
840                         for(int j = 1; j < nTracks; j++)
841                         {
842                                 CueSheetTrack *currentTrack = currentFile->track(j);
843                                 CueSheetTrack *previousTrack = currentFile->track(j-1);
844                                 double duration = currentTrack->startIndex() - previousTrack->startIndex();
845                                 previousTrack->setDuration(qMax(0.0, duration));
846                         }
847                 }
848         }
849         
850         //Sanity check of track numbers
851         if(nFiles > 0)
852         {
853                 bool hasTracks = false;
854                 int previousTrackNo = -1;
855                 bool trackNo[100];
856                 for(int i = 0; i < 100; i++)
857                 {
858                         trackNo[i] = false;
859                 }
860
861                 for(int i = 0; i < nFiles; i++)
862                 {
863                         if(application)
864                         {
865                                 application->processEvents();
866                                 Sleep(10);
867                         }
868                         CueSheetFile *currentFile = m_files.at(i);
869                         int nTracks = currentFile->trackCount();
870                         if(nTracks > 1)
871                         {
872                                 for(int j = 0; j < nTracks; j++)
873                                 {
874                                         int currentTrackNo = currentFile->track(j)->trackNo();
875                                         if(currentTrackNo > 99)
876                                         {
877                                                 qWarning("Track #%02d is invalid (maximum is 99), Cue Sheet is inconsistent!", currentTrackNo);
878                                                 return ErrorInconsistent;
879                                         }
880                                         if(currentTrackNo <= previousTrackNo)
881                                         {
882                                                 qWarning("Non-increasing track numbers (%02d -> %02d), Cue Sheet is inconsistent!", previousTrackNo, currentTrackNo);
883                                                 return ErrorInconsistent;
884                                         }
885                                         if(trackNo[currentTrackNo])
886                                         {
887                                                 qWarning("Track #%02d exists multiple times, Cue Sheet is inconsistent!", currentTrackNo);
888                                                 return ErrorInconsistent;
889                                         }
890                                         trackNo[currentTrackNo] = true;
891                                         previousTrackNo = currentTrackNo;
892                                         hasTracks = true;
893                                 }
894                         }
895                 }
896                 
897                 if(!hasTracks)
898                 {
899                         qWarning("Could not find at least one valid track in the Cue Sheet!");
900                         return ErrorInconsistent;
901                 }
902
903                 return ErrorSuccess;
904         }
905         else
906         {
907                 qWarning("Could not find at least one valid input file in the Cue Sheet!");
908                 return bUnsupportedTrack ? ErrorUnsupported : ErrorBadFile;
909         }
910 }
911
912 double CueSheetModel::parseTimeIndex(const QString &index)
913 {
914         QRegExp rxTimeIndex("\\s*(\\d+)\\s*:\\s*(\\d+)\\s*:\\s*(\\d+)\\s*");
915         
916         if(rxTimeIndex.indexIn(index) >= 0)
917         {
918                 int min, sec, frm;
919                 bool minOK, secOK, frmOK;
920
921                 min = rxTimeIndex.cap(1).toInt(&minOK);
922                 sec = rxTimeIndex.cap(2).toInt(&secOK);
923                 frm = rxTimeIndex.cap(3).toInt(&frmOK);
924
925                 if(minOK && secOK && frmOK)
926                 {
927                         return static_cast<double>(60 * min) + static_cast<double>(sec) + (static_cast<double>(frm) / 75.0);
928                 }
929         }
930
931         qWarning("    Bad time index: '%s'", index.toUtf8().constData());
932         return std::numeric_limits<double>::quiet_NaN();
933 }
934
935 QString CueSheetModel::indexToString(const double index) const
936 {
937         if(!_finite(index) || (index < 0.0) || (index > 86400.0))
938         {
939                 return QString("??:??.???");
940         }
941         
942         QTime time = QTime().addMSecs(static_cast<int>(floor(0.5 + (index * 1000.0))));
943
944         if(time.minute() < 100)
945         {
946                 return time.toString("mm:ss.zzz");
947         }
948         else
949         {
950                 return QString("99:99.999");
951         }
952 }