1 /* This file is part of Clementine. 2 Copyright 2010, David Sansome <me@davidsansome.com> 3 Clementine is free software: you can redistribute it and/or modify 4 it under the terms of the GNU General Public License as published by 5 the Free Software Foundation, either version 3 of the License, or 6 (at your option) any later version. 7 Clementine is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 GNU General Public License for more details. 11 You should have received a copy of the GNU General Public License 12 along with Clementine. If not, see <http://www.gnu.org/licenses/>. 13 */ 14 15 #include "cueparser.h" 16 17 #include <QBuffer> 18 #include <QDateTime> 19 #include <QFileInfo> 20 #include <QStringBuilder> 21 #include <QTextStream> 22 #include <QtDebug> 23 #include <QFileInfo> 24 #include <QtGlobal> 25 #include <QDebug> 26 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 27 #include <QStringConverter> 28 #include <QRegularExpression> 29 #include <QRegularExpressionMatch> 30 #else 31 #include <QTextCodec> 32 #include <QRegExp> 33 #endif 34 35 namespace Playlist { 36 static const char* kFileLineRegExp = "(\\S+)\\s+(?:\"([^\"]+)\"|(\\S+))\\s*(?:\"([^\"]+)\"|(\\S+))?"; 37 static const char* kIndexRegExp = "(\\d{2,3}):(\\d{2}):(\\d{2})"; 38 39 static const char* kPerformer = "performer"; 40 static const char* kTitle = "title"; 41 static const char* kSongWriter = "songwriter"; 42 static const char* kFile = "file"; 43 static const char* kTrack = "track"; 44 static const char* kIndex = "index"; 45 static const char* kAudioTrackType = "audio"; 46 static const char* kRem = "rem"; 47 static const char* kGenre = "genre"; 48 static const char* kDate = "date"; 49 static const char* kDisc = "discnumber"; 50 CueParser(const QString & p)51 CueParser::CueParser(const QString &p) : path(p) { 52 } 53 tracks_list() const54 QVector<Track> CueParser::tracks_list() const { 55 56 QFile device(path); 57 if (!device.open(QIODevice::ReadOnly)) { 58 qWarning() << "error opening file for reading" << path << ":" << device.errorString(); 59 return QVector<Track>(); 60 } 61 62 QTextStream text_stream(&device); 63 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 64 text_stream.setEncoding(QStringConverter::encodingForData(device.peek(1024)).value_or(QStringConverter::Utf8)); 65 #else 66 text_stream.setCodec(QTextCodec::codecForUtfText(device.peek(1024), QTextCodec::codecForName("UTF-8"))); 67 #endif 68 69 QString dir_path = QFileInfo(path).absoluteDir().absolutePath(); 70 // read the first line already 71 QString line = text_stream.readLine(); 72 73 QList<CueEntry> entries; 74 int files = 0; 75 76 QString album_artist; 77 QString album; 78 QString album_composer; 79 QString file; 80 QString file_type; 81 QString genre; 82 QString date; 83 QString disc; 84 85 // -- whole file 86 while (!text_stream.atEnd()) { 87 88 // -- FILE section 89 do { 90 QStringList splitted = split_cue_line(line); 91 92 // uninteresting or incorrect line 93 if (splitted.size() < 2) { 94 continue; 95 } 96 97 QString line_name = splitted[0].toLower(); 98 QString line_value = splitted[1]; 99 100 // PERFORMER 101 if (line_name == kPerformer) { 102 album_artist = line_value; 103 104 // TITLE 105 } else if (line_name == kTitle) { 106 album = line_value; 107 108 // SONGWRITER 109 } else if (line_name == kSongWriter) { 110 album_composer = line_value; 111 112 // FILE 113 } else if (line_name == kFile) { 114 file = QDir::isAbsolutePath(line_value) ? line_value : QDir(dir_path).absoluteFilePath(line_value); 115 116 if (splitted.size() > 2) { 117 file_type = splitted[2]; 118 } 119 120 // REM 121 } else if (line_name == kRem) { 122 if (splitted.size() < 3) { 123 break; 124 } 125 126 // REM GENRE 127 if (line_value.toLower() == kGenre) { 128 genre = splitted[2]; 129 130 // REM DATE 131 } else if (line_value.toLower() == kDate) { 132 date = splitted[2]; 133 134 // REM DISC 135 } else if (line_value.toLower() == kDisc) { 136 disc = splitted[2]; 137 } 138 139 // end of the header -> go into the track mode 140 } else if (line_name == kTrack) { 141 files++; 142 break; 143 } 144 145 // just ignore the rest of possible field types for now... 146 } while (!(line = text_stream.readLine()).isNull()); 147 148 if (line.isNull()) { 149 qWarning() << "the .cue file from " << dir_path << " defines no tracks!"; 150 //return ret; 151 } 152 153 // if this is a data file, all of it's tracks will be ignored 154 bool valid_file = file_type.compare("BINARY", Qt::CaseInsensitive) && file_type.compare("MOTOROLA", Qt::CaseInsensitive); 155 156 QString track_type; 157 QString index; 158 QString artist; 159 QString composer; 160 QString title; 161 162 // TRACK section 163 do { 164 QStringList splitted = split_cue_line(line); 165 166 // uninteresting or incorrect line 167 if (splitted.size() < 2) { 168 continue; 169 } 170 171 QString line_name = splitted[0].toLower(); 172 QString line_value = splitted[1]; 173 QString line_additional = splitted.size() > 2 ? splitted[2].toLower() : ""; 174 175 if (line_name == kTrack) { 176 // the beginning of another track's definition - we're saving the 177 // current one 178 // for later (if it's valid of course) 179 // please note that the same code is repeated just after this 'do-while' 180 // loop 181 if (valid_file && !index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) { 182 entries.append(CueEntry(file, index, title, artist, album_artist, album, composer, album_composer, genre, date, disc)); 183 } 184 185 // clear the state 186 track_type = index = artist = title = ""; 187 188 if (!line_additional.isEmpty()) { 189 track_type = line_additional; 190 } 191 192 } else if (line_name == kIndex) { 193 // we need the index's position field 194 if (!line_additional.isEmpty()) { 195 // if there's none "01" index, we'll just take the first one 196 // also, we'll take the "01" index even if it's the last one 197 if (line_value == "01" || index.isEmpty()) { 198 index = line_additional; 199 } 200 } 201 202 } else if (line_name == kPerformer) { 203 artist = line_value; 204 205 } else if (line_name == kTitle) { 206 title = line_value; 207 208 } else if (line_name == kSongWriter) { 209 composer = line_value; 210 211 // end of track's for the current file -> parse next one 212 } else if (line_name == kFile) { 213 break; 214 } 215 216 // just ignore the rest of possible field types for now... 217 } while (!(line = text_stream.readLine()).isNull()); 218 219 // we didn't add the last song yet... 220 if (valid_file && !index.isEmpty() && (track_type.isEmpty() || track_type == kAudioTrackType)) { 221 entries.append(CueEntry(file, index, title, artist, album_artist, album, composer, album_composer, genre, date, disc)); 222 } 223 } 224 225 QVector<Track> tracks; 226 227 // finalize parsing songs 228 for (int i = 0; i < entries.length(); i++) { 229 CueEntry entry = entries.at(i); 230 231 //qDebug() << entry.index << begin_by_index(entry.index) << entry.title << entry.artist << entry.album; 232 233 bool fuck; 234 Track track(entry.file, 235 begin_by_index(entry.index), 236 entry.artist.isEmpty() ? entry.album_artist : entry.artist, 237 entry.album, 238 entry.title, 239 i + 1, 240 entry.date.toUInt(&fuck), 241 0, 242 0, 243 0, 244 0); 245 track.fillAudioProperties(); 246 track.setCue(); 247 if (i < entries.length() - 1) { 248 CueEntry next_enrty = entries.at(qMin(i + 1, entries.length() - 1)); 249 qint32 duration = begin_by_index(next_enrty.index) - begin_by_index(entry.index); 250 if (duration < 0) { 251 quint32 duration = track.duration() - begin_by_index(entry.index); // last track for this file - got duration from audio properties 252 track.setDuration(duration); 253 } else { 254 track.setDuration(duration); 255 } 256 } else { 257 quint32 duration = track.duration() - begin_by_index(entry.index); // got duration from audio properties 258 track.setDuration(duration); 259 } 260 261 //qDebug() << track.track_number() << track.title() << track.artist(); 262 263 tracks << track; 264 } 265 266 return tracks; 267 } 268 269 // This and the kFileLineRegExp do most of the "dirty" work, namely: splitting 270 // the raw .cue 271 // line into logical parts and getting rid of all the unnecessary whitespaces 272 // and quoting. split_cue_line(const QString & line) const273 QStringList CueParser::split_cue_line(const QString& line) const { 274 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 275 QRegularExpression line_regexp(kFileLineRegExp); 276 QRegularExpressionMatch re_match = line_regexp.match(line.trimmed()); 277 if (!re_match.hasMatch()) { 278 return QStringList(); 279 } 280 281 // Let's remove the empty entries while we're at it 282 return re_match.capturedTexts().filter(QRegularExpression(".+")).mid(1, -1).replaceInStrings(QRegularExpression("^\"\"$"), ""); 283 #else 284 QRegExp line_regexp(kFileLineRegExp); 285 if (!line_regexp.exactMatch(line.trimmed())) { 286 return QStringList(); 287 } 288 289 // let's remove the empty entries while we're at it 290 return line_regexp.capturedTexts().filter(QRegExp(".+")).mid(1, -1); 291 #endif 292 } 293 begin_by_index(const QString & index) const294 qint32 CueParser::begin_by_index(const QString& index) const { 295 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 296 QRegularExpression index_regexp(kIndexRegExp); 297 QRegularExpressionMatch re_match = index_regexp.match(index); 298 if (!re_match.hasMatch()) { 299 return -1; 300 } 301 302 QStringList splitted = re_match.capturedTexts().mid(1, -1); 303 #else 304 QRegExp index_regexp(kIndexRegExp); 305 if (!index_regexp.exactMatch(index)) { 306 return -1; 307 } 308 309 QStringList splitted = index_regexp.capturedTexts().mid(1, -1); 310 #endif 311 312 qlonglong frames = splitted.at(0).toLongLong() * 60 * 75 + 313 splitted.at(1).toLongLong() * 75 + 314 splitted.at(2).toLongLong(); 315 316 return frames / 75; // seconds 317 } 318 } 319