3 * @author Wei-Cheng Pan
5 * Copyright (C) 2008 Wei-Cheng Pan <legnaleurc@gmail.com>
7 * This file is part of Khopper.
9 * Khopper is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
14 * Khopper is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #include "cuesheetparser.hpp"
23 #include "rangedreader.hpp"
25 #include "khopper/error.hpp"
26 #include "khopper/text.hpp"
27 #include "khopper/track.hpp"
29 #include <QtCore/QRegExp>
30 #include <QtCore/QStringList>
31 #include <QtCore/QtDebug>
32 #include <QtCore/QTextStream>
38 inline QString
stripQuote( const QString
& s
) {
39 if( s
[0] == '\"' && s
[s
.length()-1] == '\"' ) {
40 return s
.mid( 1, s
.length() - 2 );
48 using namespace khopper::album
;
49 using khopper::error::CodecError
;
50 using khopper::error::ParsingError
;
51 using khopper::codec::ReaderSP
;
52 using khopper::codec::RangedReader
;
54 PlayList
CueSheetParser::load( const QString
& content
, const QDir
& dir
) {
55 CueSheetParser
parser( content
, dir
);
56 return parser
.playList_
;
59 CueSheetParser::CueSheetParser( const QString
& content
, const QDir
& dir
):
61 album_( new CueSheet
) {
62 this->parseCue_( content
, dir
);
65 void CueSheetParser::parseCue_( QString content
, const QDir
& dir
) {
66 QRegExp
COMMENT( "\\s*REM\\s+(.*)\\s+(.*)\\s*" );
67 QRegExp
SINGLE( "\\s*(CATALOG|CDTEXTFILE|ISRC|PERFORMER|SONGWRITER|TITLE)\\s+(.*)\\s*" );
68 QRegExp
FILES( "\\s*FILE\\s+(.*)\\s+(WAVE|BINARY)\\s*" );
69 QRegExp
FLAGS( "\\s*FLAGS\\s+(DATA|DCP|4CH|PRE|SCMS)\\s*" );
70 QRegExp
TRACK( "\\s*TRACK\\s+(\\d+)\\s+(AUDIO|CDG|MODE1/2048|MODE1/2352|MODE2/2336|MODE2/2352|CDI/2336|CDI/2352)\\s*" );
71 QRegExp
INDEX( "\\s*(INDEX|PREGAP|POSTGAP)\\s+((\\d+)\\s+)?(\\d+):(\\d+):(\\d+)\\s*" );
73 this->trackIndex_
= 0;
74 QTextStream
sin( &content
, QIODevice::ReadOnly
);
76 for( QString line
= sin
.readLine(); !line
.isEmpty(); line
= sin
.readLine() ) {
77 if( SINGLE
.exactMatch( line
) ) {
78 this->parseSingle_( SINGLE
.cap( 1 ), SINGLE
.cap( 2 ) );
79 } else if( FILES
.exactMatch( line
) ) {
80 this->parseFile_( FILES
.cap( 1 ), FILES
.cap( 2 ), dir
);
81 } else if( FLAGS
.exactMatch( line
) ) {
82 this->parseFlags_( FLAGS
.cap( 1 ) );
83 } else if( INDEX
.exactMatch( line
) ) {
84 this->parseIndex_( INDEX
.cap( 1 ), INDEX
.cap( 3 ), INDEX
.cap( 4 ), INDEX
.cap( 5 ), INDEX
.cap( 6 ) );
85 } else if( COMMENT
.exactMatch( line
) ) {
86 this->parseComment_( COMMENT
.cap( 1 ), COMMENT
.cap( 2 ) );
87 } else if( TRACK
.exactMatch( line
) ) {
88 this->parseTrack_( TRACK
.cap( 1 ), TRACK
.cap( 2 ) );
90 this->parseGarbage_( line
);
95 std::for_each( this->playList_
.begin(), this->playList_
.end(), [&]( TrackSP track
) {
96 track
->setAlbum( this->album_
);
99 this->updateLastTrack_();
102 void CueSheetParser::parseSingle_( const QString
& c
, const QString
& s
) {
103 QString content
= stripQuote( s
);
104 if( c
== "CATALOG" ) {
105 this->album_
->setCatalog( content
);
106 } else if( c
== "CDTEXTFILE" ) {
107 this->album_
->setCDTextFile( content
);
108 } else if( c
== "ISRC" ) {
109 if( this->currentTrack_
) {
110 this->currentTrack_
->setISRC( content
);
112 } else if( c
== "PERFORMER" ) {
113 if( this->trackIndex_
== 0 ) {
114 this->album_
->setArtist( content
);
116 this->currentTrack_
->setArtist( content
);
118 } else if( c
== "SONGWRITER" ) {
119 if( this->trackIndex_
== 0 ) {
120 this->album_
->setSongWriter( content
);
122 this->currentTrack_
->setSongWriter( content
);
124 } else if( c
== "TITLE" ) {
125 if( this->trackIndex_
== 0 ) {
126 this->album_
->setTitle( content
);
128 this->currentTrack_
->setTitle( content
);
133 void CueSheetParser::parseFile_( const QString
& fileName
, const QString
& type
, const QDir
& dir
) {
134 if( this->trackIndex_
> 0 ) {
135 this->updateLastTrack_();
138 this->currentFilePath_
= dir
.filePath( stripQuote( fileName
) );
139 this->currentFileType_
= type
;
142 void CueSheetParser::parseFlags_( const QString
& flag
) {
143 QSet
< QString
> flags( this->album_
->getFlags() );
144 flags
.insert( flag
);
145 this->album_
->setFlags( flags
);
148 void CueSheetParser::parseIndex_( const QString
& type
, const QString
& num
, const QString
& m
, const QString
& s
, const QString
& f
) {
149 Timestamp
tmp( m
.toInt(), s
.toShort(), f
.toInt() * 1000 / 75 );
151 if( type
== "INDEX" ) {
152 short int n
= num
.toShort();
155 // starting time of pregap
156 if( this->trackIndex_
> 1 ) {
157 this->previousTrack_
->setDuration( tmp
- this->previousTrack_
->getStartTime() );
162 this->currentTrack_
->setStartTime( tmp
);
163 if( this->trackIndex_
> 1 && ( this->previousTrack_
->getStartTime() + this->previousTrack_
->getDuration() > tmp
|| !this->previousTrack_
->getDuration().isValid() ) ) {
164 this->previousTrack_
->setDuration( tmp
- this->previousTrack_
->getStartTime() );
168 // TODO: other values are subindexes
171 } else if( type
== "PREGAP" ) {
172 this->currentTrack_
->setPregap( tmp
);
173 } else if( type
== "POSTGAP" ) {
174 this->currentTrack_
->setPostgap( tmp
);
178 void CueSheetParser::parseComment_( const QString
& key
, const QString
& value
) {
179 if( this->trackIndex_
== 0 ) {
180 QStringList
comments( this->album_
->getComments() );
181 comments
.append( QString( "%1:%2;" ).arg( key
).arg( value
) );
182 this->album_
->setComments( comments
);
184 QStringList
comments( this->currentTrack_
->getComments() );
185 comments
.append( QString( "%1:%2;" ).arg( key
).arg( value
) );
186 this->currentTrack_
->setComments( comments
);
190 void CueSheetParser::parseTrack_( const QString
& num
, const QString
& type
) {
191 this->previousTrack_
= this->currentTrack_
;
192 this->currentTrack_
.reset( new CueSheetTrack( QUrl::fromLocalFile( this->currentFilePath_
) ) );
194 this->trackIndex_
= num
.toUInt();
195 this->currentTrack_
->setIndex( num
.toShort() );
196 this->currentTrack_
->setFileType( this->currentFileType_
);
197 this->currentTrack_
->setDataType( type
);
199 this->playList_
.push_back( this->currentTrack_
);
202 void CueSheetParser::parseGarbage_( const QString
& line
) {
203 if( this->trackIndex_
== 0 ) {
204 QStringList
garbage( this->album_
->getGarbage() );
205 garbage
.append( line
);
206 this->album_
->setGarbage( garbage
);
208 QStringList
garbage( this->currentTrack_
->getGarbage() );
209 garbage
.append( line
);
210 this->currentTrack_
->setGarbage( garbage
);
214 void CueSheetParser::updateLastTrack_() {
215 // get the total length, because cue sheet don't provide it
216 ReaderSP
decoder( this->currentTrack_
->createReader() );
218 // NOTE: may throw exception
219 decoder
->open( QIODevice::ReadOnly
);
220 } catch( CodecError
& e
) {
221 qDebug() << e
.getMessage();
223 if( decoder
->isOpen() ) {
224 this->currentTrack_
->setDuration( Timestamp::fromMillisecond( decoder
->getDuration() ) - this->currentTrack_
->getStartTime() );