File indexing completed on 2024-05-05 04:51:13

0001 /*
0002     SPDX-FileCopyrightText: 1998-2008 Sebastian Trueg <trueg@k3b.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "k3bcuefileparser.h"
0008 
0009 #include "k3bmsf.h"
0010 #include "k3bglobals.h"
0011 #include "k3btrack.h"
0012 #include "k3bcdtext.h"
0013 
0014 #include <QDebug>
0015 #include <QDir>
0016 #include <QFile>
0017 #include <QFileInfo>
0018 #include <QRegExp>
0019 
0020 
0021 // TODO: add method: usableByCdrecordDirectly()
0022 // TODO: add Toc with sector sizes
0023 
0024 class K3b::CueFileParser::Private
0025 {
0026 public:
0027     bool inFile;
0028     bool inTrack;
0029     Device::Track::TrackType trackType;
0030     Device::Track::DataMode trackMode;
0031     bool rawData;
0032     bool haveIndex1;
0033     K3b::Msf currentDataPos;
0034     K3b::Msf index0;
0035 
0036     K3b::Device::Toc toc;
0037     int currentParsedTrack;
0038 
0039     K3b::Device::CdText cdText;
0040 
0041     QString imageFileType;
0042 };
0043 
0044 
0045 
0046 K3b::CueFileParser::CueFileParser( const QString& filename )
0047     : K3b::ImageFileReader()
0048 {
0049     d = new Private;
0050     openFile( filename );
0051 }
0052 
0053 
0054 K3b::CueFileParser::~CueFileParser()
0055 {
0056     delete d;
0057 }
0058 
0059 
0060 void K3b::CueFileParser::readFile()
0061 {
0062     setValid(true);
0063 
0064     d->inFile = d->inTrack = d->haveIndex1 = false;
0065     d->trackMode = K3b::Device::Track::UNKNOWN;
0066     d->toc.clear();
0067     d->cdText.clear();
0068     d->currentParsedTrack = 0;
0069 
0070     QFile f( filename() );
0071     if( f.open( QIODevice::ReadOnly ) ) {
0072         while( !f.atEnd() ) {
0073             if( !parseLine( QString::fromUtf8(f.readLine() ) ) ) {
0074                 setValid(false);
0075                 break;
0076             }
0077         }
0078 
0079         if( isValid() ) {
0080             // save last parsed track for which we do not have the proper length :(
0081             if( d->currentParsedTrack > 0 ) {
0082                 d->toc.append( K3b::Device::Track( d->currentDataPos,
0083                                                  d->currentDataPos,
0084                                                  d->trackType,
0085                                                  d->trackMode ) );
0086             }
0087 
0088             // debug the toc
0089             qDebug() << "(K3b::CueFileParser) successfully parsed cue file." << Qt::endl
0090                      << "------------------------------------------------" << Qt::endl;
0091             for( int i = 0; i < d->toc.count(); ++i ) {
0092                 K3b::Device::Track& track = d->toc[i];
0093                 qDebug() << "Track " << (i+1)
0094                          << " (" << ( track.type() == K3b::Device::Track::TYPE_AUDIO ? "audio" : "data" ) << ") "
0095                          << track.firstSector().toString() << " - " << track.lastSector().toString() << Qt::endl;
0096             }
0097 
0098             qDebug() << "------------------------------------------------";
0099         }
0100     }
0101     else {
0102         qDebug() << "(K3b::CueFileParser) could not open file " << filename();
0103         setValid(false);
0104     }
0105 }
0106 
0107 
0108 bool K3b::CueFileParser::parseLine( QString line )
0109 {
0110     // use cap(1) for the filename
0111     static QRegExp fileRx( "FILE\\s\"?([^\"].*[^\"\\s])\"?\\s\"?(.*)\"?" );
0112 
0113     // use cap(1) for the flags
0114     static QRegExp flagsRx( "FLAGS(\\s(DCP|4CH|PRE|SCMS)){1,4}" );
0115 
0116     // use cap(1) for the tracknumber and cap(2) for the datatype
0117     static QRegExp trackRx( "TRACK\\s(\\d{1,2})\\s(AUDIO|CDG|MODE1/2048|MODE1/2352|MODE2/2336|MODE2/2352|CDI/2336|CDI/2352)" );
0118 
0119     // use cap(1) for the index number, cap(3) for the minutes, cap(4) for the seconds, cap(5) for the frames,
0120     // and cap(2) for the MSF value string
0121     static QRegExp indexRx( "INDEX\\s(\\d{1,2})\\s((\\d+):([0-5]\\d):((?:[0-6]\\d)|(?:7[0-4])))" );
0122 
0123     // use cap(1) for the MCN
0124     static QRegExp catalogRx( "CATALOG\\s(\\w{13,13})" );
0125 
0126     // use cap(1) for the ISRC
0127     static QRegExp isrcRx( "ISRC\\s(\\w{5,5}\\d{7,7})" );
0128 
0129     static QString cdTextRxStr = "\"?([^\"]{0,80})\"?";
0130 
0131     // use cap(1) for the string
0132     static QRegExp titleRx( "TITLE\\s" + cdTextRxStr );
0133     static QRegExp performerRx( "PERFORMER\\s" + cdTextRxStr );
0134     static QRegExp songwriterRx( "SONGWRITER\\s" + cdTextRxStr );
0135 
0136 
0137     // simplify all white spaces except those in filenames and CD-TEXT
0138     simplified( line );
0139 
0140     // skip comments and empty lines
0141     if( line.startsWith("REM") || line.startsWith('#') || line.isEmpty() )
0142         return true;
0143 
0144 
0145     //
0146     // FILE
0147     //
0148     if( fileRx.exactMatch( line ) ) {
0149 
0150         setValid( findImageFileName( fileRx.cap(1) ) );
0151 
0152         if( d->inFile ) {
0153             qDebug() << "(K3b::CueFileParser) only one FILE statement allowed.";
0154             return false;
0155         }
0156         d->inFile = true;
0157         d->inTrack = false;
0158         d->haveIndex1 = false;
0159         d->imageFileType = fileRx.cap( 2 ).toLower();
0160         return true;
0161     }
0162 
0163 
0164     //
0165     // TRACK
0166     //
0167     else if( trackRx.exactMatch( line ) ) {
0168         if( !d->inFile ) {
0169             qDebug() << "(K3b::CueFileParser) TRACK statement before FILE.";
0170             return false;
0171         }
0172 
0173         // check if we had index1 for the last track
0174         if( d->inTrack && !d->haveIndex1 ) {
0175             qDebug() << "(K3b::CueFileParser) TRACK without INDEX 1.";
0176             return false;
0177         }
0178 
0179         // save last track
0180         // TODO: use d->rawData in some way
0181         if( d->currentParsedTrack > 0 ) {
0182             d->toc.append( K3b::Device::Track( d->currentDataPos,
0183                                              d->currentDataPos,
0184                                              d->trackType,
0185                                              d->trackMode ) );
0186         }
0187 
0188         d->currentParsedTrack++;
0189 
0190         // parse the tracktype
0191         if( trackRx.cap(2) == "AUDIO" ) {
0192             d->trackType = K3b::Device::Track::TYPE_AUDIO;
0193             d->trackMode = K3b::Device::Track::UNKNOWN;
0194         }
0195         else {
0196             d->trackType = K3b::Device::Track::TYPE_DATA;
0197             if( trackRx.cap(2).startsWith("MODE1") ) {
0198                 d->trackMode = K3b::Device::Track::MODE1;
0199                 d->rawData = (trackRx.cap(2) == "MODE1/2352");
0200             }
0201             else if( trackRx.cap(2).startsWith("MODE2") ) {
0202                 d->trackMode = K3b::Device::Track::MODE2;
0203                 d->rawData = (trackRx.cap(2) == "MODE2/2352");
0204             }
0205             else {
0206                 qDebug() << "(K3b::CueFileParser) unsupported track type: " << trackRx.cap(2);
0207                 return false;
0208             }
0209         }
0210 
0211         d->haveIndex1 = false;
0212         d->inTrack = true;
0213         d->index0 = 0;
0214 
0215         return true;
0216     }
0217 
0218 
0219     //
0220     // FLAGS
0221     //
0222     else if( flagsRx.exactMatch( line ) ) {
0223         if( !d->inTrack ) {
0224             qDebug() << "(K3b::CueFileParser) FLAGS statement without TRACK.";
0225             return false;
0226         }
0227 
0228         // TODO: save the flags
0229         return true;
0230     }
0231 
0232 
0233     //
0234     // INDEX
0235     //
0236     else if( indexRx.exactMatch( line ) ) {
0237         if( !d->inTrack ) {
0238             qDebug() << "(K3b::CueFileParser) INDEX statement without TRACK.";
0239             return false;
0240         }
0241 
0242         unsigned int indexNumber = indexRx.cap(1).toInt();
0243 
0244         K3b::Msf indexStart = K3b::Msf::fromString( indexRx.cap(2) );
0245 
0246         if( indexNumber == 0 ) {
0247             d->index0 = indexStart;
0248 
0249             if( d->currentParsedTrack < 2 && indexStart > 0 ) {
0250                 qDebug() << "(K3b::CueFileParser) first track is not allowed to have a pregap > 0.";
0251                 return false;
0252             }
0253         }
0254         else if( indexNumber == 1 ) {
0255             d->haveIndex1 = true;
0256             d->currentDataPos = indexStart;
0257             if( d->currentParsedTrack > 1 ) {
0258                 d->toc[d->currentParsedTrack-2].setLastSector( indexStart-1 );
0259                 if( d->index0 > 0 && d->index0 < indexStart ) {
0260                     d->toc[d->currentParsedTrack-2].setIndex0( d->index0 - d->toc[d->currentParsedTrack-2].firstSector() );
0261                 }
0262             }
0263         }
0264         else {
0265             // TODO: add index > 0
0266         }
0267 
0268         return true;
0269     }
0270 
0271 
0272     //
0273     // CATALOG
0274     //
0275     if( catalogRx.exactMatch( line ) ) {
0276         // TODO: set the toc's mcn
0277         return true;
0278     }
0279 
0280 
0281     //
0282     // ISRC
0283     //
0284     if( isrcRx.exactMatch( line ) ) {
0285         if( d->inTrack ) {
0286             // TODO: set the track's ISRC
0287             return true;
0288         }
0289         else {
0290             qDebug() << "(K3b::CueFileParser) ISRC without TRACK.";
0291             return false;
0292         }
0293     }
0294 
0295 
0296     //
0297     // CD-TEXT
0298     // TODO: create K3b::Device::TrackCdText entries
0299     //
0300     else if( titleRx.exactMatch( line ) ) {
0301         if( d->inTrack )
0302             d->cdText[d->currentParsedTrack-1].setTitle( titleRx.cap(1) );
0303         else
0304             d->cdText.setTitle( titleRx.cap(1) );
0305         return true;
0306     }
0307 
0308     else if( performerRx.exactMatch( line ) ) {
0309         if( d->inTrack )
0310             d->cdText[d->currentParsedTrack-1].setPerformer( performerRx.cap(1) );
0311         else
0312             d->cdText.setPerformer( performerRx.cap(1) );
0313         return true;
0314     }
0315 
0316     else if( songwriterRx.exactMatch( line ) ) {
0317         if( d->inTrack )
0318             d->cdText[d->currentParsedTrack-1].setSongwriter( songwriterRx.cap(1) );
0319         else
0320             d->cdText.setSongwriter( songwriterRx.cap(1) );
0321         return true;
0322     }
0323 
0324     else {
0325         qDebug() << "(K3b::CueFileParser) unknown Cue line: '" << line << "'";
0326         return false;
0327     }
0328 }
0329 
0330 
0331 void K3b::CueFileParser::simplified( QString& s )
0332 {
0333     s = s.trimmed();
0334 
0335     int i = 0;
0336     bool insideQuote = false;
0337     while( i < s.length() ) {
0338         if( !insideQuote ) {
0339             if( s[i].isSpace() && s[i+1].isSpace() )
0340                 s.remove( i, 1 );
0341         }
0342 
0343         if( s[i] == '"' )
0344             insideQuote = !insideQuote;
0345 
0346         ++i;
0347     }
0348 }
0349 
0350 
0351 K3b::Device::Toc K3b::CueFileParser::toc() const
0352 {
0353     return d->toc;
0354 }
0355 
0356 
0357 K3b::Device::CdText K3b::CueFileParser::cdText() const
0358 {
0359     return d->cdText;
0360 }
0361 
0362 
0363 QString K3b::CueFileParser::imageFileType() const
0364 {
0365     return d->imageFileType;
0366 }
0367 
0368 
0369 bool K3b::CueFileParser::findImageFileName( const QString& dataFile )
0370 {
0371     //
0372     // CDRDAO does not use this image filename but replaces the extension from the cue file
0373     // with "bin" to get the image filename, we should take this into account
0374     //
0375 
0376     qDebug() << "(K3b::CueFileParser) trying to find file: " << dataFile;
0377 
0378     m_imageFilenameInCue = true;
0379 
0380     // first try filename as a whole (absolute)
0381     if( QFile::exists( dataFile ) ) {
0382         setImageFilename( QFileInfo(dataFile).absoluteFilePath() );
0383         return true;
0384     }
0385 
0386     // try the filename in the cue's directory
0387     if( QFileInfo( K3b::parentDir(filename()) + dataFile.section( '/', -1 ) ).isFile() ) {
0388         setImageFilename( K3b::parentDir(filename()) + dataFile.section( '/', -1 ) );
0389         qDebug() << "(K3b::CueFileParser) found image file: " << imageFilename();
0390         return true;
0391     }
0392 
0393     // try the filename ignoring case
0394     if( QFileInfo( K3b::parentDir(filename()) + dataFile.section( '/', -1 ).toLower() ).isFile() ) {
0395         setImageFilename( K3b::parentDir(filename()) + dataFile.section( '/', -1 ).toLower() );
0396         qDebug() << "(K3b::CueFileParser) found image file: " << imageFilename();
0397         return true;
0398     }
0399 
0400     m_imageFilenameInCue = false;
0401 
0402     // try removing the ending from the cue file (image.bin.cue and image.bin)
0403     if( QFileInfo( filename().left( filename().length()-4 ) ).isFile() ) {
0404         setImageFilename( filename().left( filename().length()-4 ) );
0405         qDebug() << "(K3b::CueFileParser) found image file: " << imageFilename();
0406         return true;
0407     }
0408 
0409     //
0410     // we did not find the image specified in the cue.
0411     // Search for another one having the same filename as the cue but a different extension
0412     //
0413 
0414     QDir parentDir( K3b::parentDir(filename()) );
0415     QString filenamePrefix = filename().section( '/', -1 );
0416     filenamePrefix.truncate( filenamePrefix.length() - 3 ); // remove cue extension
0417     qDebug() << "(K3b::CueFileParser) checking folder " << parentDir.path() << " for files: " << filenamePrefix << "*";
0418 
0419     //
0420     // we cannot use the nameFilter in QDir because of the spaces that may occur in filenames
0421     //
0422     QStringList possibleImageFiles = parentDir.entryList( QDir::Files );
0423     int cnt = 0;
0424     for( QStringList::const_iterator it = possibleImageFiles.constBegin(); it != possibleImageFiles.constEnd(); ++it ) {
0425         if( (*it).toLower() == dataFile.section( '/', -1 ).toLower() ||
0426             ((*it).startsWith( filenamePrefix ) && !(*it).endsWith( "cue" )) ) {
0427             ++cnt;
0428             setImageFilename( K3b::parentDir(filename()) + *it );
0429         }
0430     }
0431 
0432     //
0433     // we only do this if there is one unique file which fits the requirements.
0434     // Otherwise we cannot be certain to have the right file.
0435     //
0436     return ( cnt == 1 && QFileInfo( imageFilename() ).isFile() );
0437 }