OSDN Git Service

Updated changelog.
[lamexp/LameXP.git] / src / Model_CueSheet.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2023 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; always including the non-optional
9 // LAMEXP GNU GENERAL PUBLIC LICENSE ADDENDUM. See "License.txt" file!
10 //
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License along
17 // with this program; if not, write to the Free Software Foundation, Inc.,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 //
20 // http://www.gnu.org/licenses/gpl-2.0.txt
21 ///////////////////////////////////////////////////////////////////////////////
22
23 //Internal
24 #include "Global.h"
25 #include "Model_CueSheet.h"
26 #include "Model_AudioFile.h"
27 #include "Genres.h"
28
29 //MUtils
30 #include <MUtils/Global.h>
31 #include <MUtils/OSSupport.h>
32
33 //Qt
34 #include <QApplication>
35 #include <QDir>
36 #include <QFileInfo>
37 #include <QFont>
38 #include <QTime>
39 #include <QTextCodec>
40 #include <QTextStream>
41 #include <QMutexLocker>
42
43 //CRT
44 #include <float.h>
45 #include <limits>
46
47 #define UNQUOTE(STR) STR.split("\"",  QString::SkipEmptyParts).first().trimmed()
48
49 ////////////////////////////////////////////////////////////
50 // Helper Classes
51 ////////////////////////////////////////////////////////////
52
53 class CueSheetItem
54 {
55 public:
56         virtual const char* type(void) = 0;
57         virtual bool isValid(void) { return false; }
58 };
59
60 class CueSheetTrack : public CueSheetItem
61 {
62 public:
63         CueSheetTrack(CueSheetFile *parent, int trackNo)
64         :
65                 m_parent(parent)
66         {
67                 m_startIndex = std::numeric_limits<double>::quiet_NaN();
68                 m_duration = std::numeric_limits<double>::infinity();
69                 m_metaInfo.setPosition(trackNo);
70         }
71         
72         //Getter
73         CueSheetFile *parent(void) { return m_parent; }
74         double startIndex(void) { return m_startIndex; }
75         double duration(void) { return m_duration; }
76         AudioFileModel_MetaInfo &metaInfo(void) { return m_metaInfo; }
77
78         //Setter
79         void setStartIndex(double startIndex) { m_startIndex = startIndex; }
80         void setDuration(double duration) { m_duration = duration; }
81         
82         //Misc
83         virtual bool isValid(void) { return !(_isnan(m_startIndex) || (m_metaInfo.position() == 0)); }
84         virtual const char* type(void) { return "CueSheetTrack"; }
85
86 private:
87         double m_startIndex;
88         double m_duration;
89         AudioFileModel_MetaInfo m_metaInfo;
90         CueSheetFile *const m_parent;
91 };
92
93 class CueSheetFile : public CueSheetItem
94 {
95 public:
96         CueSheetFile(const QString &fileName) : m_fileName(fileName) {}
97         ~CueSheetFile(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
98         
99         //Getter
100         QString fileName(void) { return m_fileName; }
101         CueSheetTrack *track(int index) { return m_tracks.at(index); }
102         int trackCount(void) { return m_tracks.count(); }
103
104         //Modifier
105         void addTrack(CueSheetTrack *track) { m_tracks.append(track); }
106         void clearTracks(void) { while(!m_tracks.isEmpty()) delete m_tracks.takeLast(); }
107
108         //Misc
109         virtual bool isValid(void) { return m_tracks.count() > 0; }
110         virtual const char* type(void) { return "CueSheetFile"; }
111
112 private:
113         const QString m_fileName;
114         QList<CueSheetTrack*> m_tracks;
115 };
116
117 ////////////////////////////////////////////////////////////
118 // Constructor & Destructor
119 ////////////////////////////////////////////////////////////
120
121 QMutex CueSheetModel::m_mutex(QMutex::Recursive);
122
123 CueSheetModel::CueSheetModel()
124 :
125         m_fileIcon(":/icons/music.png"),
126         m_trackIcon(":/icons/control_play_blue.png")
127 {
128         /*nothing to do*/
129 }
130
131 CueSheetModel::~CueSheetModel(void)
132 {
133         while(!m_files.isEmpty()) delete m_files.takeLast();
134 }
135
136 ////////////////////////////////////////////////////////////
137 // Model Functions
138 ////////////////////////////////////////////////////////////
139
140 QModelIndex CueSheetModel::index(int row, int column, const QModelIndex &parent) const
141 {
142         QMutexLocker lock(&m_mutex);
143         
144         if(!parent.isValid())
145         {
146                 return (row < m_files.count()) ? createIndex(row, column, m_files.at(row)) : QModelIndex();
147         }
148
149         CueSheetItem *parentItem = static_cast<CueSheetItem*>(parent.internalPointer());
150         if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(parentItem))
151         {
152                 return (row < filePtr->trackCount()) ? createIndex(row, column, filePtr->track(row)) : QModelIndex();
153         }
154
155         return QModelIndex();
156 }
157
158 int CueSheetModel::columnCount(const QModelIndex& /*parent*/) const
159 {
160         QMutexLocker lock(&m_mutex);
161         return 4;
162 }
163
164 int CueSheetModel::rowCount(const QModelIndex &parent) const
165 {
166         QMutexLocker lock(&m_mutex);
167
168         if(!parent.isValid())
169         {
170                 return m_files.count();
171         }
172
173         CueSheetItem *parentItem = static_cast<CueSheetItem*>(parent.internalPointer());
174         if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(parentItem))
175         {
176                 return filePtr->trackCount();
177         }
178
179         return 0;
180 }
181
182 QModelIndex CueSheetModel::parent(const QModelIndex &child) const
183 {
184         QMutexLocker lock(&m_mutex);
185         
186         if(child.isValid())
187         {
188                 CueSheetItem *childItem = static_cast<CueSheetItem*>(child.internalPointer());
189                 if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(childItem))
190                 {
191                         return createIndex(m_files.indexOf(trackPtr->parent()), 0, trackPtr->parent());
192                 }
193         }
194
195         return QModelIndex();
196 }
197
198 QVariant CueSheetModel::headerData (int section, Qt::Orientation /*orientation*/, int role) const
199 {
200         QMutexLocker lock(&m_mutex);
201         
202         if(role == Qt::DisplayRole)
203         {
204                 switch(section)
205                 {
206                 case 0:
207                         return tr("No.");
208                         break;
209                 case 1:
210                         return tr("File / Track");
211                         break;
212                 case 2:
213                         return tr("Index");
214                         break;
215                 case 3:
216                         return tr("Duration");
217                         break;
218                 default:
219                         return QVariant();
220                         break;
221                 }
222         }
223         else
224         {
225                 return QVariant();
226         }
227 }
228
229 QVariant CueSheetModel::data(const QModelIndex &index, int role) const
230 {
231         QMutexLocker lock(&m_mutex);
232
233         if(role == Qt::DisplayRole)
234         {
235                 CueSheetItem *item = reinterpret_cast<CueSheetItem*>(index.internalPointer());
236
237                 if(CueSheetFile *filePtr = dynamic_cast<CueSheetFile*>(item))
238                 {
239                         switch(index.column())
240                         {
241                         case 0:
242                                 return tr("File %1").arg(QString().sprintf("%02d", index.row() + 1)).append(" ");
243                                 break;
244                         case 1:
245                                 return QFileInfo(filePtr->fileName()).fileName();
246                                 break;
247                         default:
248                                 return QVariant();
249                                 break;
250                         }
251                 }
252                 else if(CueSheetTrack *trackPtr = dynamic_cast<CueSheetTrack*>(item))
253                 {
254                         const AudioFileModel_MetaInfo &trackInfo = trackPtr->metaInfo();
255                         switch(index.column())
256                         {
257                         case 0:
258                                 return tr("Track %1").arg(QString().sprintf("%02d", trackInfo.position())).append(" ");
259                                 break;
260                         case 1:
261                                 if(!trackInfo.title().isEmpty() && !trackInfo.artist().isEmpty())
262                                 {
263                                         return QString("%1 - %2").arg(trackInfo.artist(), trackInfo.title());
264                                 }
265                                 else if(!trackInfo.title().isEmpty())
266                                 {
267                                         return QString("%1 - %2").arg(tr("Unknown Artist"), trackInfo.title());
268                                 }
269                                 else if(!trackInfo.artist().isEmpty())
270                                 {
271                                         return QString("%1 - %2").arg(trackInfo.artist(), 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 const AudioFileModel_MetaInfo *CueSheetModel::getTrackInfo(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)->metaInfo();
409                 }
410         }
411
412         return NULL;
413 }
414
415 bool 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                         return true;
431                 }
432         }
433
434         return false;
435 }
436
437 const AudioFileModel_MetaInfo *CueSheetModel::getAlbumInfo(void)
438 {
439         QMutexLocker lock(&m_mutex);
440         return &m_albumInfo;
441 }
442
443 ////////////////////////////////////////////////////////////
444 // Cue Sheet Parser
445 ////////////////////////////////////////////////////////////
446
447 int CueSheetModel::loadCueSheet(const QString &cueFileName, QCoreApplication *application, QTextCodec *forceCodec)
448 {
449         QMutexLocker lock(&m_mutex);
450         const QTextCodec *codec = (forceCodec != NULL) ? forceCodec : QTextCodec::codecForName("System");
451         
452         QFile cueFile(cueFileName);
453         if(!cueFile.open(QIODevice::ReadOnly))
454         {
455                 return ErrorIOFailure;
456         }
457
458         clearData();
459
460         beginResetModel();
461         int iResult = parseCueFile(cueFile, QDir(QFileInfo(cueFile).canonicalPath()), application, codec);
462         endResetModel();
463
464         return iResult;
465 }
466
467 int CueSheetModel::parseCueFile(QFile &cueFile, const QDir &baseDir, QCoreApplication *application, const QTextCodec *codec)
468 {
469         cueFile.reset();
470         qDebug("\n[Cue Sheet Import]");
471         bool bForceLatin1 = false;
472
473         //Reject very large files, as parsing might take until forever
474         if(cueFile.size() >= 10485760i64)
475         {
476                 qWarning("File is very big. Probably not a Cue Sheet. Rejecting...");
477                 return 2;
478         }
479
480         //Test selected Codepage for decoding errors
481         qDebug("Character encoding is: %s.", codec->name().constData());
482         const QString replacementSymbol = QString(QChar(QChar::ReplacementCharacter));
483         QByteArray testData = cueFile.peek(1048576);
484         if((!testData.isEmpty()) && codec->toUnicode(testData.constData(), testData.size()).contains(replacementSymbol))
485         {
486                 qWarning("Decoding error using selected codepage (%s). Enforcing Latin-1.", codec->name().constData());
487                 bForceLatin1 = true;
488         }
489         testData.clear();
490
491         //Init text stream
492         QTextStream cueStream(&cueFile);
493         cueStream.setAutoDetectUnicode(false);
494         cueStream.setCodec(bForceLatin1 ? "latin1" : codec->name());
495         cueStream.seek(0i64);
496
497         //Create regular expressions
498         QRegExp rxFile("^FILE\\s+(\"[^\"]+\"|\\S+)\\s+(\\w+)$", Qt::CaseInsensitive);
499         QRegExp rxTrack("^TRACK\\s+(\\d+)\\s(\\w+)$", Qt::CaseInsensitive);
500         QRegExp rxIndex("^INDEX\\s+(\\d+)\\s+([0-9:]+)$", Qt::CaseInsensitive);
501         QRegExp rxTitle("^TITLE\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive);
502         QRegExp rxPerformer("^PERFORMER\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive);
503         QRegExp rxGenre("^REM\\s+GENRE\\s+(\"[^\"]+\"|\\S+)$", Qt::CaseInsensitive);
504         QRegExp rxYear("^REM\\s+DATE\\s+(\\d+)$", Qt::CaseInsensitive);
505         
506         bool bPreamble = true;
507         bool bUnsupportedTrack = false;
508
509         CueSheetFile *currentFile = NULL;
510         CueSheetTrack *currentTrack = NULL;
511
512         m_albumInfo.reset();
513
514         //Loop over the Cue Sheet until all lines were processed
515         for(int lines = 0; lines < INT_MAX; lines++)
516         {
517                 if(application)
518                 {
519                         application->processEvents();
520                         if(lines < 128) MUtils::OS::sleep_ms(10);
521                 }
522                 
523                 if(cueStream.atEnd())
524                 {
525                         qDebug("End of Cue Sheet file.");
526                         break;
527                 }
528
529                 QString line = cueStream.readLine().trimmed();
530                 
531                 /* --- FILE --- */
532                 if(rxFile.indexIn(line) >= 0)
533                 {
534                         qDebug("%03d File: <%s> <%s>", lines, MUTILS_UTF8(rxFile.cap(1)), MUTILS_UTF8(rxFile.cap(2)));
535                         if(currentFile)
536                         {
537                                 if(currentTrack)
538                                 {
539                                         if(currentTrack->isValid())
540                                         {
541                                                 currentFile->addTrack(currentTrack);
542                                                 currentTrack = NULL;
543                                         }
544                                         else
545                                         {
546                                                 MUTILS_DELETE(currentTrack);
547                                         }
548                                 }
549                                 if(currentFile->isValid())
550                                 {
551                                         m_files.append(currentFile);
552                                         currentFile = NULL;
553                                 }
554                                 else
555                                 {
556                                         MUTILS_DELETE(currentFile);
557                                 }
558                         }
559                         else
560                         {
561                                 MUTILS_DELETE(currentTrack);
562                         }
563                         if(!rxFile.cap(2).compare("WAVE", Qt::CaseInsensitive) || !rxFile.cap(2).compare("MP3", Qt::CaseInsensitive) || !rxFile.cap(2).compare("AIFF", Qt::CaseInsensitive))
564                         {
565                                 currentFile = new CueSheetFile(baseDir.absoluteFilePath(UNQUOTE(rxFile.cap(1))));
566                                 qDebug("%03d File path: <%s>", lines, currentFile->fileName().toUtf8().constData());
567                         }
568                         else
569                         {
570                                 bUnsupportedTrack = true;
571                                 qWarning("%03d Skipping unsupported file of type '%s'.", lines, MUTILS_UTF8(rxFile.cap(2)));
572                                 currentFile = NULL;
573                         }
574                         bPreamble = false;
575                         currentTrack = NULL;
576                         continue;
577                 }
578                 
579                 /* --- TRACK --- */
580                 if(rxTrack.indexIn(line) >= 0)
581                 {
582                         if(currentFile)
583                         {
584                                 qDebug("%03d   Track: <%s> <%s>", lines, MUTILS_UTF8(rxTrack.cap(1)), MUTILS_UTF8(rxTrack.cap(2)));
585                                 if(currentTrack)
586                                 {
587                                         if(currentTrack->isValid())
588                                         {
589                                                 currentFile->addTrack(currentTrack);
590                                                 currentTrack = NULL;
591                                         }
592                                         else
593                                         {
594                                                 MUTILS_DELETE(currentTrack);
595                                         }
596                                 }
597                                 if(!rxTrack.cap(2).compare("AUDIO", Qt::CaseInsensitive))
598                                 {
599                                         currentTrack = new CueSheetTrack(currentFile, rxTrack.cap(1).toInt());
600                                 }
601                                 else
602                                 {
603                                         bUnsupportedTrack = true;
604                                         qWarning("%03d   Skipping unsupported track of type '%s'.", lines, MUTILS_UTF8(rxTrack.cap(2)));
605                                         currentTrack = NULL;
606                                 }
607                         }
608                         else
609                         {
610                                 MUTILS_DELETE(currentTrack);
611                         }
612                         bPreamble = false;
613                         continue;
614                 }
615                 
616                 /* --- INDEX --- */
617                 if(rxIndex.indexIn(line) >= 0)
618                 {
619                         if(currentFile && currentTrack)
620                         {
621                                 qDebug("%03d     Index: <%s> <%s>", lines, MUTILS_UTF8(rxIndex.cap(1)), MUTILS_UTF8(rxIndex.cap(2)));
622                                 if(rxIndex.cap(1).toInt() == 1)
623                                 {
624                                         currentTrack->setStartIndex(parseTimeIndex(rxIndex.cap(2)));
625                                 }
626                         }
627                         continue;
628                 }
629
630                 /* --- TITLE --- */
631                 if(rxTitle.indexIn(line) >= 0)
632                 {
633                         if(bPreamble)
634                         {
635                                 m_albumInfo.setAlbum(UNQUOTE(rxTitle.cap(1)).simplified());
636                         }
637                         else if(currentFile && currentTrack)
638                         {
639                                 qDebug("%03d     Title: <%s>", lines, MUTILS_UTF8(rxTitle.cap(1)));
640                                 currentTrack->metaInfo().setTitle(UNQUOTE(rxTitle.cap(1)).simplified());
641                         }
642                         continue;
643                 }
644
645                 /* --- PERFORMER --- */
646                 if(rxPerformer.indexIn(line) >= 0)
647                 {
648                         if(bPreamble)
649                         {
650                                 m_albumInfo.setArtist(UNQUOTE(rxPerformer.cap(1)).simplified());
651                         }
652                         else if(currentFile && currentTrack)
653                         {
654                                 qDebug("%03d     Title: <%s>", lines, MUTILS_UTF8(rxPerformer.cap(1)));
655                                 currentTrack->metaInfo().setArtist(UNQUOTE(rxPerformer.cap(1)).simplified());
656                         }
657                         continue;
658                 }
659
660                 /* --- GENRE --- */
661                 if(rxGenre.indexIn(line) >= 0)
662                 {
663                         if(bPreamble)
664                         {
665                                 QString temp = UNQUOTE(rxGenre.cap(1)).simplified();
666                                 for(int i = 0; g_lamexp_generes[i]; i++)
667                                 {
668                                         if(temp.compare(g_lamexp_generes[i], Qt::CaseInsensitive) == 0)
669                                         {
670                                                 m_albumInfo.setGenre(QString(g_lamexp_generes[i]));
671                                                 break;
672                                         }
673                                 }
674                         }
675                         else if(currentFile && currentTrack)
676                         {
677                                 qDebug("%03d     Genre: <%s>", lines, MUTILS_UTF8(rxGenre.cap(1)));
678                                 QString temp = UNQUOTE(rxGenre.cap(1).simplified());
679                                 for(int i = 0; g_lamexp_generes[i]; i++)
680                                 {
681                                         if(temp.compare(g_lamexp_generes[i], Qt::CaseInsensitive) == 0)
682                                         {
683                                                 currentTrack->metaInfo().setGenre(QString(g_lamexp_generes[i]));
684                                                 break;
685                                         }
686                                 }
687                         }
688                         continue;
689                 }
690
691                 /* --- YEAR --- */
692                 if(rxYear.indexIn(line) >= 0)
693                 {
694                         if(bPreamble)
695                         {
696                                 bool ok = false;
697                                 unsigned int temp = rxYear.cap(1).toUInt(&ok);
698                                 if(ok) m_albumInfo.setYear(temp);
699                         }
700                         else if(currentFile && currentTrack)
701                         {
702                                 qDebug("%03d     Year: <%s>", lines, MUTILS_UTF8(rxPerformer.cap(1)));
703                                 bool ok = false;
704                                 unsigned int temp = rxYear.cap(1).toUInt(&ok);
705                                 if(ok) currentTrack->metaInfo().setYear(temp);
706                         }
707                         continue;
708                 }
709         }
710
711         //Append the very last track/file that is still pending
712         if(currentFile)
713         {
714                 if(currentTrack)
715                 {
716                         if(currentTrack->isValid())
717                         {
718                                 currentFile->addTrack(currentTrack);
719                                 currentTrack = NULL;
720                         }
721                         else
722                         {
723                                 MUTILS_DELETE(currentTrack);
724                         }
725                 }
726                 if(currentFile->isValid())
727                 {
728                         m_files.append(currentFile);
729                         currentFile = NULL;
730                 }
731                 else
732                 {
733                         MUTILS_DELETE(currentFile);
734                 }
735         }
736
737         //Finally calculate duration of each track
738         int nFiles = m_files.count();
739         for(int i = 0; i < nFiles; i++)
740         {
741                 if(application)
742                 {
743                         application->processEvents();
744                         MUtils::OS::sleep_ms(10);
745                 }
746
747                 currentFile = m_files.at(i);
748                 int nTracks = currentFile->trackCount();
749                 if(nTracks > 1)
750                 {
751                         for(int j = 1; j < nTracks; j++)
752                         {
753                                 currentTrack = currentFile->track(j);
754                                 CueSheetTrack *previousTrack = currentFile->track(j-1);
755                                 double duration = currentTrack->startIndex() - previousTrack->startIndex();
756                                 previousTrack->setDuration(qMax(0.0, duration));
757                         }
758                 }
759         }
760         
761         //Sanity check of track numbers
762         if(nFiles > 0)
763         {
764                 bool hasTracks = false;
765                 int previousTrackNo = -1;
766                 bool trackNo[100];
767                 for(int i = 0; i < 100; i++)
768                 {
769                         trackNo[i] = false;
770                 }
771
772                 for(int i = 0; i < nFiles; i++)
773                 {
774                         if(application)
775                         {
776                                 application->processEvents();
777                                 MUtils::OS::sleep_ms(10);
778                         }
779                         currentFile = m_files.at(i);
780                         int nTracks = currentFile->trackCount();
781                         if(nTracks > 1)
782                         {
783                                 for(int j = 0; j < nTracks; j++)
784                                 {
785                                         int currentTrackNo = currentFile->track(j)->metaInfo().position();
786                                         if(currentTrackNo > 99)
787                                         {
788                                                 qWarning("Track #%02d is invalid (maximum is 99), Cue Sheet is inconsistent!", currentTrackNo);
789                                                 return ErrorInconsistent;
790                                         }
791                                         if(currentTrackNo <= previousTrackNo)
792                                         {
793                                                 qWarning("Non-increasing track numbers (%02d -> %02d), Cue Sheet is inconsistent!", previousTrackNo, currentTrackNo);
794                                                 return ErrorInconsistent;
795                                         }
796                                         if(trackNo[currentTrackNo])
797                                         {
798                                                 qWarning("Track #%02d exists multiple times, Cue Sheet is inconsistent!", currentTrackNo);
799                                                 return ErrorInconsistent;
800                                         }
801                                         trackNo[currentTrackNo] = true;
802                                         previousTrackNo = currentTrackNo;
803                                         hasTracks = true;
804                                 }
805                         }
806                 }
807                 
808                 if(!hasTracks)
809                 {
810                         qWarning("Could not find at least one valid track in the Cue Sheet!");
811                         return ErrorInconsistent;
812                 }
813
814                 return ErrorSuccess;
815         }
816         else
817         {
818                 qWarning("Could not find at least one valid input file in the Cue Sheet!");
819                 return bUnsupportedTrack ? ErrorUnsupported : ErrorBadFile;
820         }
821 }
822
823 double CueSheetModel::parseTimeIndex(const QString &index)
824 {
825         QRegExp rxTimeIndex("\\s*(\\d+)\\s*:\\s*(\\d+)\\s*:\\s*(\\d+)\\s*");
826         
827         if(rxTimeIndex.indexIn(index) >= 0)
828         {
829                 int time[3];
830                 if(MUtils::regexp_parse_int32(rxTimeIndex, time, 3))
831                 {
832                         return static_cast<double>(60 * time[0]) + static_cast<double>(time[1]) + (static_cast<double>(time[2]) / 75.0);
833                 }
834         }
835
836         qWarning("    Bad time index: '%s'", MUTILS_UTF8(index));
837         return std::numeric_limits<double>::quiet_NaN();
838 }
839
840 QString CueSheetModel::indexToString(const double index) const
841 {
842         if((!_finite(index)) || (index < 0.0) || (index > 86400.0))
843         {
844                 return QString("??:??.???");
845         }
846         
847         const int minutes = static_cast<int>(index / 60.0);
848         if(minutes < 100)
849         {
850                 const MUtils::fp_parts_t seconds = MUtils::break_fp(fmod(index, 60.0));
851                 return QString().sprintf("%02d:%02d.%03d", minutes, static_cast<int>(seconds.parts[0]), static_cast<int>(seconds.parts[1] * 1000.0));
852         }
853
854         return QString("99:99.999");
855 }