[Feature] Improve uri handling.
[Khopper.git] / plugins / cuesheet / cuesheetparser.cpp
blob392033ea77d3051a7bbcadd3ed26d4bf1d6826e3
1 /**
2 * @file cuesheet.cpp
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>
34 #include <algorithm>
36 namespace {
38 inline QString stripQuote( const QString & s ) {
39 if( s[0] == '\"' && s[s.length()-1] == '\"' ) {
40 return s.mid( 1, s.length() - 2 );
41 } else {
42 return s;
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 ):
60 playList_(),
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 ) );
89 } else {
90 this->parseGarbage_( line );
94 // set track album
95 std::for_each( this->playList_.begin(), this->playList_.end(), [&]( TrackSP track ) {
96 track->setAlbum( this->album_ );
97 } );
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 );
115 } else {
116 this->currentTrack_->setArtist( content );
118 } else if( c == "SONGWRITER" ) {
119 if( this->trackIndex_ == 0 ) {
120 this->album_->setSongWriter( content );
121 } else {
122 this->currentTrack_->setSongWriter( content );
124 } else if( c == "TITLE" ) {
125 if( this->trackIndex_ == 0 ) {
126 this->album_->setTitle( content );
127 } else {
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();
153 switch( n ) {
154 case 0:
155 // starting time of pregap
156 if( this->trackIndex_ > 1 ) {
157 this->previousTrack_->setDuration( tmp - this->previousTrack_->getStartTime() );
159 break;
160 case 1:
161 // track start time
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() );
166 break;
167 default:
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 );
183 } else {
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 );
207 } else {
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() );
217 try {
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() );
226 decoder->close();