1 /*
2  * Cantata
3  *
4  * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5  *
6  */
7 /* This file is part of Clementine.
8    Copyright 2010, David Sansome <me@davidsansome.com>
9 
10    Clementine is free software: you can redistribute it and/or modify
11    it under the terms of the GNU General Public License as published by
12    the Free Software Foundation, either version 3 of the License, or
13    (at your option) any later version.
14 
15    Clementine is distributed in the hope that it will be useful,
16    but WITHOUT ANY WARRANTY; without even the implied warranty of
17    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18    GNU General Public License for more details.
19 
20    You should have received a copy of the GNU General Public License
21    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
22 */
23 
24 #include "cuefile.h"
25 #include "mpdconnection.h"
26 #include "support/utils.h"
27 #include <QBuffer>
28 #include <QDateTime>
29 #include <QFile>
30 #include <QDir>
31 #include <QFileInfo>
32 #include <QStringBuilder>
33 #include <QRegExp>
34 #include <QRegularExpression>
35 #include <QTextCodec>
36 #include <QTextStream>
37 #include <QStringList>
38 #include <QUrl>
39 #include <QUrlQuery>
40 #include <QObject>
41 
42 #include <QDebug>
43 static bool debugEnabled=false;
44 #define DBUG if (debugEnabled) qWarning() << "CueFile"
enableDebug()45 void CueFile::enableDebug()
46 {
47     debugEnabled=true;
48 }
49 
50 static const QString constCueProtocol = QLatin1String("cue:///");
51 static const QString constFile = QLatin1String("file");
52 static const QString constAudioTrackType = QLatin1String("audio");
53 static const QString constGenre = QLatin1String("genre");
54 static const QString constDate = QLatin1String("date");
55 static const QString constOrigYear = QLatin1String("originalyear");
56 static const QString constOrigDate = QLatin1String("originaldate");
57 static const QString constDisc = QLatin1String("disc");
58 static const QString constDiscNumber = QLatin1String("discnumber");
59 static const QString constRemark = QLatin1String("rem");
60 static const QString constComment = QLatin1String("comment");
61 static const QString constTrack = QLatin1String("track");
62 static const QString constIndex = QLatin1String("index");
63 static const QString constTitle = QLatin1String("title");
64 static const QString constComposer = QLatin1String("composer");
65 static const QString constPerformer = QLatin1String("performer");
66 
isCue(const QString & str)67 bool CueFile::isCue(const QString &str)
68 {
69     return str.startsWith(constCueProtocol);
70 }
71 
getLoadLine(const QString & str)72 QByteArray CueFile::getLoadLine(const QString &str)
73 {
74     QUrl u(str);
75     QUrlQuery q(u);
76 
77     if (q.hasQueryItem("pos")) {
78         QString pos=q.queryItemValue("pos");
79         QString path=u.path();
80         if (path.startsWith("/")) {
81             path=path.mid(1);
82         }
83         return MPDConnection::encodeName(path)+" \""+pos.toLatin1()+":"+QString::number(pos.toInt()+1).toLatin1()+"\"";
84     }
85 
86     return MPDConnection::encodeName(str);
87 }
88 
codecList()89 static const QList<QTextCodec *> & codecList()
90 {
91     static QList<QTextCodec *> codecs;
92     if (codecs.isEmpty()) {
93         codecs.append(QTextCodec::codecForName("UTF-8"));
94         QTextCodec *codec=QTextCodec::codecForLocale();
95         if (codec && !codecs.contains(codec)) {
96             codecs.append(codec);
97         }
98         codec=QTextCodec::codecForName("System");
99         if (codec && !codecs.contains(codec)) {
100             codecs.append(codec);
101         }
102     }
103     return codecs;
104 }
105 
106 // Split a raw .cue line into logical parts, returning a list where:
107 //   the 1st item contains 'REM' if present (if not present it is empty),
108 //   the 2nd item contains the CUE command (which is empty only for the standard 'REM comment' commands)
109 //   the 3rd item contains the remaining part in the line
splitCueLine(const QString & line)110 static QStringList splitCueLine(const QString &line)
111 {
112     QRegularExpression reCueLine;
113     reCueLine.setPattern("^\\s*(REM){0,1}\\s*([a-zA-Z]*){0,1}\\s*(.*)$");
114     reCueLine.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
115     reCueLine.setPatternOptions(QRegularExpression::DotMatchesEverythingOption); // dot metacharacter (.) match everything including newlines
116 
117     QRegularExpressionMatch m = reCueLine.match(line);
118     if (m.hasMatch()) {
119         return { m.captured(1), m.captured(2), m.captured(3) };
120     } else {
121         return QStringList();
122     }
123 }
124 
125 static const double constMsecPerSec = 1000.0;
126 
127 // Seconds in a MSF index (MM:SS:FF)
indexToMarker(const QString & index)128 static double indexToMarker(const QString &index)
129 {
130     QRegExp indexRegexp("(\\d{1,3}):(\\d{2}):(\\d{2})");
131     if (!indexRegexp.exactMatch(index)) {
132         return -1.0;
133     }
134 
135     QStringList splitted = indexRegexp.capturedTexts().mid(1, -1);
136     qlonglong frames = splitted.at(0).toLongLong() * 60 * 75 + splitted.at(1).toLongLong() * 75 + splitted.at(2).toLongLong();
137     return (frames * constMsecPerSec) / 75.0;
138 }
139 
140 // Updates the time (in seconds) using the indexes of the tracks (songs).
141 // This one mustn't be used for the last track (song).
indexTime(const QString & index,const QString & nextIndex,int & time)142 static bool indexTime(const QString &index, const QString &nextIndex, int &time)
143 {
144     double beginning = indexToMarker(index);
145     double end = indexToMarker(nextIndex);
146 
147     // incorrect indices (we won't be able to calculate beginning or end)
148     if (beginning<0 || end<0) {
149         DBUG << "Failed to calculate time - index:" << index << "nextIndex:" << nextIndex << "beginning:" << beginning << "end:" << end;
150         return false;
151     }
152     // calculate time duration in seconds
153     time=static_cast<int>(((end-beginning)/constMsecPerSec)+0.5);
154     return true;
155 }
156 
157 // Updates the lastTrackIndex time (in seconds) using the index of the last track (song).
158 // This one must be used ONLY for the last track (song).
159 //      note: the last song time will be calculate dinamically later
160 //            (see: MPDParseUtils::parseDirItems in mpd-interface/mpdparseutils.cpp )
indexLastTime(const QString & index,double & lastTrackIndex)161 static bool indexLastTime(const QString &index, double &lastTrackIndex)
162 {
163     double beginning = indexToMarker(index);
164 
165     // incorrect index (we won't be able to calculate beginning)
166     if (beginning<0) {
167         DBUG << "Failed to calculate *last* time - index:" << index << "beginning:" << beginning;
168         return false;
169     }
170     // note: the last song duration will be calculate dinamically
171     //       (see: MPDParseUtils::parseDirItems in mpd-interface/mpdparseutils.cpp )
172     lastTrackIndex = beginning;
173     return true;
174 }
175 
176 // Is various artists?
isVariousArtists(const QString & str)177 static inline bool isVariousArtists(const QString &str)
178 {
179     if (0==str.compare(Song::variousArtists(), Qt::CaseInsensitive)) {
180         return true;
181     }
182 
183     QString lower = str.toLower();
184     return QLatin1String("various") == lower || QLatin1String("various artists") == lower;
185 }
186 
187 // Parse CUE file content
188 //
189 // Supported CUE tags                       | corresponding MPD tags
190 // -------------------
191 // FILE HEADER SECTION
192 // -------------------
193 // REM GENRE "genre"                        | genre
194 // REM DATE "YYYY"                          | date
195 // REM ORIGINALYEAR "YYYY"                  | originadate
196 // REM ORIGINALDATE "YYYY"                  |
197 // REM DISC "[N]N"                          | disc
198 // REM DISCNUMBER "[N]N"                    |
199 // REM "comment"                            | comment
200 // REM COMMENT "comment" [relaxed syntax]   |
201 // REM COMPOSER "composer"                  | composer (see NOTES)
202 // COMPOSER "composer" [relaxed syntax]     |
203 // PERFORMER "performer"                    | artist (see NOTES)
204 // TITLE "album"                            | album
205 // FILE "filename" filetype
206 // ----------------
207 // TRACK(S SECTION
208 // ----------------
209 // TRACK NN datatype
210 // INDEX NN MM:SS:FF
211 // TITLE "title"                            | title
212 // REM COMPOSER "composer"                  | composer (see NOTES)
213 // COMPOSER "composer" [relaxed syntax]     |
214 // PERFORMER "performer"                    | performer (see NOTES)
215 //
216 // NOTES:
217 // -----
218 //
219 // - The official CUESHEET specification does not offer a specific command to indicate the album artist;
220 //   and the MPD documentation says: "albumartist: On multi-artist albums, this is the artist name which
221 //   shall be used for the whole album (the exact meaning of this tag is not well-defined)."
222 //
223 // - The MPD documentation says: "artist: The artist name (its meaning is not well-defined; see composer
224 //   and performer for more specific tags)."
225 //   The official CUESHEET specification by CDRWIN state the rule: "If the PERFORMER command appears
226 //   before any TRACK commands, then the string will be encoded as the performer of the entire disc. If the
227 //   command appears after a TRACK command, then the string will be encoded as the performer of the current track."
228 //   The PERFORMER command, when present in the header section of a CUE file, is generally and conventionally
229 //   used with reference to the entire album to indicate the artist (band or singer), or the composer (and not
230 //   who is performing the song) in the case of genres such is classical music.
231 //   Therefore:
232 //   * when in the header section of a CUE file, PERFORMER should mean the artist to whom the album refers to;
233 //   * when in the tracks section of a CUE file, PERFORMER should mean the artist performing the song.
234 //
235 // - MPD has a composer tag, but the official CUESHEET specification does not offer a specific command
236 //   to indicate the composer; nevertheless, especially in the CUE files associated with classical music CDs,
237 //   the REM COMPOSER (which is a standard-compliant use trick of the REM command) or the COMPOSER (relaxed
238 //   non-compliant syntax) is sometimes used, mainly in the tracks section.
239 //   Similar to PERFORMER, a compliant rule for COMPOSER should be: "If the COMPOSER command appears before
240 //   any TRACK commands, then the string will be encoded as the composer of the entire disc. If the command
241 //   appears after a TRACK command, then the string will be encoded as the composer of the current track."
242 //   Therefore:
243 //   * when in the header section of a CUE file, COMPOSER should mean the composer to whom the album refers to;
244 //   * when in the tracks section of a CUE file, COMPOSER should mean the composer of the song.
245 //
parse(const QString & fileName,const QString & dir,QList<Song> & songList,QSet<QString> & files,double & lastTrackIndex)246 bool CueFile::parse(const QString &fileName, const QString &dir, QList<Song> &songList, QSet<QString> &files, double &lastTrackIndex)
247 {
248     DBUG << fileName;
249 
250     // CUE file stream
251     QScopedPointer<QTextStream> textStream;
252     QString decoded;
253     QFile f(dir+fileName);
254     if (f.open(QIODevice::ReadOnly)) {
255         // First attempt to use QTextDecoder to decode cue file contents into a QString
256         QByteArray contents=f.readAll();
257         for (QTextCodec *codec: codecList()) {
258             QTextDecoder decoder(codec);
259             decoded=decoder.toUnicode(contents);
260             if (!decoder.hasFailure()) {
261                 textStream.reset(new QTextStream(&decoded, QIODevice::ReadOnly));
262                 break;
263             }
264         }
265         f.close();
266 
267         if (!textStream) {
268             decoded.clear();
269             // Failed to use text decoders, fall back to old method...
270             f.open(QIODevice::ReadOnly|QIODevice::Text);
271             textStream.reset(new QTextStream(&f));
272             textStream->setCodec(QTextCodec::codecForUtfText(f.peek(1024), QTextCodec::codecForName("UTF-8")));
273         }
274     }
275 
276     if (!textStream) {
277         return false;
278     }
279 
280     // file dir
281     QString fileDir=fileName.contains("/") ? Utils::getDir(fileName) : QString();
282 
283     // vars to store the current values from the lines in the FILE (header) section of the CUE file
284     QStringList genre;
285     QString album;
286     QString albumArtist;
287     QString date;
288     QString origYear;
289     QString discNo;
290     //// QString remark;
291     // auxiliary vars for other data coming from the FILE section of the CUE file
292     QString albumComposer;
293     // variables to store the current values from the lines in the TRACKs section of the CUE file
294     QString file;
295     QString trackNo;
296     QString trackType;
297     QString index;
298     QString title;
299     QString artist; // is PERFORMER in the CUE file
300     QString composer;
301     // auxiliary data for handling the TRACKs section of the CUE file
302     QString fileType;
303     bool isValidFile = false;
304 
305     // hash to save a CUE track ("cuecommandtag", "cuecommandvalue")
306     typedef QHash<QString, QString> CueTrack;
307     // initialize the hash
308     //      note: using reserve() here should NOT be necessary since QHash'automatically shrinks or grows to provide
309     //            good performance without wasting memory (see: https://doc.qt.io/qt-5/qhash.html#reserve)
310     CueTrack cueTrack;
311 
312     // vector where to save CUE tracks hashes (Red Book standard: CDDA can contain up to 99 tracks...)
313     QVector<CueTrack> tracks;
314     // note: reserve() prevent reallocations and memory fragmentation (see: https://doc.qt.io/qt-5/qvector.html#reserve)
315     tracks.reserve(99);
316 
317     // initialize vars for handling parsing flow
318     QString line;
319     bool isCueHeader=true;
320 
321     // -- parse whole file
322     while (!textStream->atEnd()) {
323         // read current line removing whitespace characters from the start and the end
324         line = textStream->readLine().trimmed();
325         DBUG << line;
326 
327         // if current line is empty then skip the line
328         if (line.isEmpty()) {
329             continue;
330         }
331 
332         QStringList splitted = splitCueLine(line);
333         // incorrect line
334         if (splitted.size() < 3 ) {
335             continue;
336         }
337         // logical parts in the CUE line
338         QString cmdRem = splitted[0].toLower();
339         QString cmdCmd = splitted[1].toLower();
340         QString cmdVal = splitted[2].trimmed();
341         if (cmdVal.startsWith("\"") && cmdVal.endsWith("\"")) {
342             cmdVal = cmdVal.mid(1, cmdVal.size()-2).trimmed();
343         }
344         DBUG << "cmdRem:" << cmdRem << ", cmdCmd:" << cmdCmd << ", cmdVal:" << cmdVal;
345 
346         // -- FILE section
347         if (cmdCmd == constFile) {
348             // get the file type
349             if (!cmdVal.isEmpty()) {
350                 cmdVal.remove('"').remove("'");
351                 QStringList cmdValSplitted = cmdVal.split(QRegExp("\\s+"));
352                 if (cmdValSplitted.size() == 2) {
353                     file = cmdValSplitted[0].remove("\"");  // file audio: name
354                     fileType = cmdValSplitted[1];           // file audio: type
355                 }
356             }
357             // check if is a valid audio file (else if this is a data file, all of it's tracks will be ignored...)
358             isValidFile = fileType.compare("BINARY", Qt::CaseInsensitive) && fileType.compare("MOTOROLA", Qt::CaseInsensitive);
359 
360             DBUG << "FILE: file:" << file << ", fileType:" << fileType << ", fileValid?" << isValidFile;
361             DBUG << "HEADER: genre:" << genre << ", album:" << album << ", discNo:" << discNo << ", year:" << date << ", originalYear:" << origYear
362                  << ", albumArtist:" << albumArtist << ", albumComposer:" << albumComposer;
363 
364             // now we are going to the TRACKs section...
365             isCueHeader = false;
366             // jump to next line which will be the first in the TRACKs section...
367             continue;
368         }
369         if (isCueHeader) {
370             // this is a standard 'REM comment' OR a tricky 'REM COMMENT comment'
371             if ((cmdRem == constRemark && cmdCmd.isEmpty()) || cmdCmd == constComment) {
372                 // skip comments since are not handled by the Cantata library db...
373                 continue;
374             // continue parsing the FILE section (header of the CUE file)...
375             } else if (cmdCmd == constGenre) {
376                 // if GENRE is a list (separated by one of: , ; | \t), then split
377                 for (const auto &g: cmdVal.split(QRegExp("(,|;|\\t|\\|)"))) {
378                     genre.append(g.trimmed());
379                 }
380             } else if (cmdCmd == constTitle) {
381                 album = cmdVal;
382             } else if (cmdCmd == constDate) {
383                 date = cmdVal.length()>4 ? cmdVal.left(4) : cmdVal;
384             } else if (cmdCmd == constOrigDate || cmdCmd == constOrigYear) {
385                 origYear = cmdVal.length()>4 ? cmdVal.left(4) : cmdVal;
386             } else if (cmdCmd == constDisc || cmdCmd == constDiscNumber) {
387                 discNo = cmdVal;
388             } else if (cmdCmd == constPerformer) {
389                 albumArtist = cmdVal;
390                 // if a VA album, use the standard VA string
391                 if (isVariousArtists(albumArtist)) {
392                     albumArtist = Song::variousArtists();
393                 }
394             } else if (cmdCmd == constComposer) {
395                 albumComposer = cmdVal;
396                 // if a VA album, use the standard VA string
397                 if (isVariousArtists(albumComposer)) {
398                     albumComposer = Song::variousArtists();
399                 }
400             // } else if (...) {
401             // ... just ignore the rest of possible field types for now...
402             }
403         // -- TRACKs section
404         } else {
405             // the beginning of a track's definition
406             if (cmdCmd == constTrack) {
407                 // if there is a *previous* track and is a *valid* track, then finalize it...
408                 //      note: a track is valid when the file which it belongs is a valid audio file (not BINARY or MOTOROLA)
409                 //            AND has an index AND is an audio (AUDIO) track
410                 // the code section starting below is repeated after, in order to save the last track also
411                 //      note: Cantata has code (Song::albumArtistOrComposer) which checks if use artist (performer) or
412                 //            composer based upon the 'Composer support' tweak setting...
413 
414                 // -- start of repeated code --
415                 if (isValidFile && !index.isEmpty() && (trackType == constAudioTrackType || trackType.isEmpty())) {
416                     DBUG << "CUE tracks:" << tracks.size();
417                     // finalize albumArtist, artist, and composer...
418                     if (artist.isEmpty() && !albumArtist.isEmpty() && !isVariousArtists(albumArtist)) {
419                         artist = albumArtist;
420                     }
421                     if (composer.isEmpty() && !albumComposer.isEmpty() && !isVariousArtists(albumComposer)) {
422                         composer = albumComposer;
423                     }
424                     // set fallbacks...
425                     if (albumArtist.isEmpty()) {
426                         albumArtist = Song::unknown();
427                     }
428                     if (artist.isEmpty()) {
429                         artist = Song::unknown();
430                     }
431                     if (composer.isEmpty()) {
432                         composer = Song::unknown();
433                     }
434                     if (title.isEmpty()) {
435                         title = Song::unknown();
436                     }
437                     DBUG << "file:" << file  << ", trackNo:" << trackNo << ", title:" << title << ", type:" << trackType << ", index:" << index
438                          << ", artist:" << artist << ", composer:" << composer;
439                     // update track hash values
440                     //      note: using insert() means that if there is already an item with the key, its value is replaced with the
441                     //            new value avoiding allocation of unnecessary items (see: https://doc.qt.io/qt-5/qhash.html#insert)
442                     cueTrack.insert("file",file);
443                     cueTrack.insert("trackNo",trackNo);
444                     cueTrack.insert("index",index);
445                     cueTrack.insert("title",title);
446                     cueTrack.insert("artist",artist);
447                     cueTrack.insert("composer",composer);
448                     // save track
449                     //     note: using append() means growing without reallocating the entire vector each time (see: https://doc.qt.io/qt-5/qvector.html#append)
450                     tracks.append(cueTrack);
451                     // clear the state for the next track
452                     trackType = trackNo = index = title = artist = composer = QString();
453                 }
454                 // -- end of repeated code --
455 
456                 // here is a new track... get the actual track number and the track type
457                 if (!cmdVal.isEmpty()) {
458                     cmdVal.remove('"').remove("'");
459                     QStringList cmdValSplitted = cmdVal.split(QRegExp("\\s+"));
460                     if (cmdValSplitted.size() == 2) {
461                         trackNo = cmdValSplitted[0];
462                         trackType = cmdValSplitted[1].toLower();
463                     }
464                 }
465 
466             } else if (cmdCmd == constIndex) {
467                 // we need the index's position field
468                 //      note: only the "01" index is considered
469                 //      note: PREGAP and POSTGAP are NOT handled...
470                 if (!cmdVal.isEmpty()) {
471                     cmdVal.remove('"').remove("'");
472                     QStringList cmdValSplitted = cmdVal.split(QRegExp("\\s+"));
473                     // if there's none "01" index, we'll just take the first one
474                     // also, we'll take the "01" index even if it's the last one
475                     if (cmdValSplitted.size() == 2 && (cmdValSplitted[0]==QLatin1String("01") || cmdValSplitted[0]==QLatin1String("1") || index.isEmpty())) {
476                         index = cmdValSplitted[1];
477                     }
478                 }
479             } else if (cmdCmd == constTitle && !cmdVal.isEmpty()) {
480                 title = cmdVal;
481             } else if (cmdCmd == constPerformer && !cmdVal.isEmpty()) {
482                 artist = cmdVal;  // is PERFORMER in the CUE file
483             } else if (cmdCmd == constComposer && !cmdVal.isEmpty()) {
484                 composer = cmdVal;
485             // } else if (...) {
486             // ... just ignore the rest of possible field types for now...
487             }
488         }
489     // the next line in the CUE file...
490     }
491 
492     // we didn't add the last track yet... (repeating code)
493 
494     // -- start of repeated code --
495     if (isValidFile && !index.isEmpty() && (trackType == constAudioTrackType || trackType.isEmpty())) {
496         DBUG << "CUE tracks:" << tracks.size();
497         // finalize albumArtist, artist, and composer...
498         if (artist.isEmpty() && !albumArtist.isEmpty() && !isVariousArtists(albumArtist)) {
499             artist = albumArtist;
500         }
501         if (composer.isEmpty() && !albumComposer.isEmpty() && !isVariousArtists(albumComposer)) {
502             composer = albumComposer;
503         }
504         // set fallbacks...
505         if (albumArtist.isEmpty()) {
506             albumArtist = Song::unknown();
507         }
508         if (artist.isEmpty()) {
509             artist = Song::unknown();
510         }
511         if (composer.isEmpty()) {
512             composer = Song::unknown();
513         }
514         if (title.isEmpty()) {
515             title = Song::unknown();
516         }
517         DBUG << "file:" << file << ", trackNo:" << trackNo << ", title:" << title << ", type:" << trackType << ", index:" << index
518              << ", artist:" << artist << ", composer:" << composer;
519         // update track hash values
520         //      note: using insert() means that if there is already an item with the key, its value is replaced with the
521         //            new value avoiding allocation of unnecessary items (see: https://doc.qt.io/qt-5/qhash.html#insert)
522         cueTrack.insert("file",file);
523         cueTrack.insert("trackNo",trackNo);
524         cueTrack.insert("index",index);
525         cueTrack.insert("title",title);
526         cueTrack.insert("artist",artist);
527         cueTrack.insert("composer",composer);
528         // save track
529         //     note: using append() means growing without reallocating the entire vector each time (see: https://doc.qt.io/qt-5/qvector.html#append)
530         tracks.append(cueTrack);
531         // clear the state for the next track
532         trackType = trackNo = index = title = artist = composer = QString();
533     }
534     // -- end of repeated code --
535 
536     DBUG << "CUE tracks:" << tracks.size();
537 
538     // check if the CUE file has valid tracks...
539     if (tracks.size() == 0) {
540         return false;
541     }
542 
543     // finalize songs
544     for (int i = 0; i < tracks.size(); i++) {
545         // note: using at() to lookup values from the track vector is slightly faster than
546         //       using operator[] or value() (see: https://doc.qt.io/qt-5/qvector.html#value)
547         // note: using value() to lookup values from the track hash is the recommended way
548         //       (see: https://doc.qt.io/qt-5/qhash.html#details)
549         Song song;
550         song.file=constCueProtocol+fileName+"?pos="+QString::number(i);
551         song.disc=static_cast<quint8>(discNo.toUInt());
552         song.track=static_cast<quint8>(tracks.at(i).value("trackNo").toUInt());
553         QString songFile=fileDir+tracks.at(i).value("file");
554         song.setName(songFile); // HACK!!!
555         if (!files.contains(songFile)) {
556             files.insert(songFile);
557         }
558         // set time...
559         //      note: the last TRACK for every FILE gets it's 'end' marker from the media file's length
560         //            (this will be calculated elsewhere when calling MPDParseUtils::parseDirItems)
561         if (i+1 < tracks.size() && tracks.at(i).value("file") == tracks[i+1].value("file")) {
562             int time=0;
563             // incorrect indices?
564             if (!indexTime(tracks.at(i).value("index"), tracks[i+1].value("index"), time)) {
565                 continue;  // if incorrect, then skip the track (jump to the next one)
566             }
567             song.time = static_cast<quint16>(time);
568         } else {
569             // incorrect index?
570             if (!indexLastTime(tracks.at(i).value("index"), lastTrackIndex)) {
571                 continue;  // if incorrect, then skip the track (jump to the next one)
572             }
573         }
574         // now finalize remaining tags...
575         if (genre.isEmpty()) {
576             song.addGenre(Song::unknown());
577         } else {
578             for (const auto &g: genre) {
579                 song.addGenre(g);
580             }
581         }
582         song.album=album;
583         song.albumartist=albumArtist;
584         song.year=static_cast<quint16>(date.toUInt());
585         song.origYear=static_cast<quint16>(origYear.toUInt());
586         song.title=tracks.at(i).value("title");
587         song.artist=tracks.at(i).value("artist");
588         if (!tracks.at(i).value("composer").isEmpty()) {
589             song.setComposer(tracks.at(i).value("composer"));
590         }
591 //        if (!remark.isEmpty()) {
592 //            song.setComment(remark.trimmed());
593 //        }
594         DBUG << "SONG file:" << song.file << ", songFile:" << songFile << ", genre:" << song.displayGenre() << ", album:" << song.album
595              << ", year:" << song.year << ", originalYear:" << song.origYear << ", discNo:" << song.disc << ", trackNo:" << song.track
596              << ", title:" << song.title << ", albumArtist:" << song.albumartist << ", artist:" << song.artist << ", composer:" << song.composer();
597 
598         // finished updating this song: append!!!
599         songList.append(song);
600     }
601 
602     return true;
603 }
604