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