OSDN Git Service

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