1 /*
2  * Cantata
3  *
4  * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5  *
6  */
7 /*
8  * Copyright (c) 2008 Sander Knopper (sander AT knopper DOT tk) and
9  *                    Roeland Douma (roeland AT rullzer DOT com)
10  *
11  * This file is part of QtMPC.
12  *
13  * QtMPC is free software: you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation, either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * QtMPC is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with QtMPC.  If not, see <http://www.gnu.org/licenses/>.
25  */
26 
27 #include <QList>
28 #include <QString>
29 #include <QStringList>
30 #include <QUrl>
31 #include <QFile>
32 #include "online/onlineservice.h"
33 #include "online/podcastservice.h"
34 #include "mpdparseutils.h"
35 #include "mpdstatus.h"
36 #include "mpdstats.h"
37 #include "playlist.h"
38 #include "song.h"
39 #include "output.h"
40 #ifdef ENABLE_HTTP_SERVER
41 #include "http/httpserver.h"
42 #endif
43 #include "support/utils.h"
44 #include "cuefile.h"
45 #include "mpdconnection.h"
46 #include <algorithm>
47 
48 #include <QDebug>
49 static bool debugEnabled=false;
50 #define DBUG if (debugEnabled) qWarning() << "MPDParseUtils"
enableDebug()51 void MPDParseUtils::enableDebug()
52 {
53     debugEnabled=true;
54 }
55 
56 static const QByteArray constTimeKey("Time: ");
57 static const QByteArray constAlbumKey("Album: ");
58 static const QByteArray constArtistKey("Artist: ");
59 static const QByteArray constAlbumArtistKey("AlbumArtist: ");
60 static const QByteArray constAlbumSortKey("AlbumSort: ");
61 static const QByteArray constArtistSortKey("ArtistSort: ");
62 static const QByteArray constAlbumArtistSortKey("AlbumArtistSort: ");
63 static const QByteArray constComposerKey("Composer: ");
64 static const QByteArray constPerformerKey("Performer: ");
65 static const QByteArray constCommentKey("Comment: ");
66 static const QByteArray constTitleKey("Title: ");
67 static const QByteArray constTrackKey("Track: ");
68 static const QByteArray constIdKey("Id: ");
69 static const QByteArray constDiscKey("Disc: ");
70 static const QByteArray constDateKey("Date: ");
71 static const QByteArray constOriginalDateKey("OriginalDate: ");
72 static const QByteArray constGenreKey("Genre: ");
73 static const QByteArray constNameKey("Name: ");
74 static const QByteArray constPriorityKey("Prio: ");
75 static const QByteArray constAlbumId("MUSICBRAINZ_ALBUMID: ");
76 static const QByteArray constFileKey("file: ");
77 static const QByteArray constPlaylistKey("playlist: ");
78 static const QByteArray constDirectoryKey("directory: ");
79 static const QByteArray constOutputIdKey("outputid: ");
80 static const QByteArray constOutputNameKey("outputname: ");
81 static const QByteArray constOutputEnabledKey("outputenabled: ");
82 static const QByteArray constChangePosKey("cpos");
83 static const QByteArray constChangeIdKey("Id");
84 static const QByteArray constLastModifiedKey("Last-Modified: ");
85 static const QByteArray constStatsArtistsKey("artists: ");
86 static const QByteArray constStatsAlbumsKey("albums: ");
87 static const QByteArray constStatsSongsKey("songs: ");
88 static const QByteArray constStatsUptimeKey("uptime: ");
89 static const QByteArray constStatsPlaytimeKey("playtime: ");
90 static const QByteArray constStatsDbPlaytimeKey("db_playtime: ");
91 static const QByteArray constStatsDbUpdateKey("db_update: ");
92 
93 static const QByteArray constStatusVolumeKey("volume: ");
94 static const QByteArray constStatusConsumeKey("consume: ");
95 static const QByteArray constStatusRepeatKey("repeat: ");
96 static const QByteArray constStatusSingleKey("single: ");
97 static const QByteArray constStatusRandomKey("random: ");
98 static const QByteArray constStatusPlaylistKey("playlist: ");
99 static const QByteArray constStatusPlaylistLengthKey("playlistlength: ");
100 static const QByteArray constStatusCrossfadeKey("xfade: ");
101 static const QByteArray constStatusStateKey("state: ");
102 static const QByteArray constStatusSongKey("song: ");
103 static const QByteArray constStatusSongIdKey("songid: ");
104 static const QByteArray constStatusNextSongKey("nextsong: ");
105 static const QByteArray constStatusNextSongIdKey("nextsongid: ");
106 static const QByteArray constStatusTimeKey("time: ");
107 static const QByteArray constStatusBitrateKey("bitrate: ");
108 static const QByteArray constStatusAudioKey("audio: ");
109 static const QByteArray constStatusUpdatingDbKey("updating_db: ");
110 static const QByteArray constStatusErrorKey("error: ");
111 static const QByteArray constChannel("channel: ");
112 static const QByteArray constMessage("message: ");
113 
114 static const QByteArray constOkValue("OK");
115 static const QByteArray constSetValue("1");
116 static const QByteArray constPlayValue("play");
117 static const QByteArray constStopValue("stop");
118 
119 static const QString constProtocol=QLatin1String("://");
120 static const QString constHttpProtocol=QLatin1String("http://");
121 
toBool(const QByteArray & v)122 static inline bool toBool(const QByteArray &v) { return v==constSetValue; }
123 
124 static QSet<QString> singleTracksFolders;
125 static MPDParseUtils::CueSupport cueSupport=MPDParseUtils::Cue_Parse;
126 
toCueSupport(const QString & str)127 MPDParseUtils::CueSupport MPDParseUtils::toCueSupport(const QString &str)
128 {
129     for (int i=0; i<Cue_Count; ++i) {
130         if (toStr((MPDParseUtils::CueSupport)i)==str) {
131             return (MPDParseUtils::CueSupport)i;
132         }
133     }
134     return MPDParseUtils::Cue_Parse;
135 }
136 
toStr(MPDParseUtils::CueSupport cs)137 QString MPDParseUtils::toStr(MPDParseUtils::CueSupport cs)
138 {
139     switch (cs) {
140     default:
141     case MPDParseUtils::Cue_Parse:            return QLatin1String("parse");
142     case MPDParseUtils::Cue_ListButDontParse: return QLatin1String("list");
143     case MPDParseUtils::Cue_Ignore:           return QLatin1String("ignore");
144     }
145 }
146 
setCueFileSupport(CueSupport cs)147 void MPDParseUtils::setCueFileSupport(CueSupport cs)
148 {
149     cueSupport=cs;
150 }
151 
cueFileSupport()152 MPDParseUtils::CueSupport MPDParseUtils::cueFileSupport()
153 {
154     return cueSupport;
155 }
156 
setSingleTracksFolders(const QSet<QString> & folders)157 void MPDParseUtils::setSingleTracksFolders(const QSet<QString> &folders)
158 {
159     singleTracksFolders=folders;
160 }
161 
parsePlaylists(const QByteArray & data)162 QList<Playlist> MPDParseUtils::parsePlaylists(const QByteArray &data)
163 {
164     QList<Playlist> playlists;
165     QList<QByteArray> lines = data.split('\n');
166     int amountOfLines = lines.size();
167 
168     for (int i = 0; i < amountOfLines; i++) {
169         const QByteArray &line=lines.at(i);
170 
171         if (line.startsWith(constPlaylistKey)) {
172             Playlist playlist;
173             playlist.name = QString::fromUtf8(line.mid(constPlaylistKey.length()));
174             i++;
175 
176             if (i < amountOfLines) {
177                 const QByteArray &line=lines.at(i);
178                 if (line.startsWith(constLastModifiedKey)) {
179                     playlist.lastModified=QDateTime::fromString(QString::fromUtf8(line.mid(constLastModifiedKey.length())), Qt::ISODate);
180                     playlists.append(playlist);
181                 }
182             }
183         }
184     }
185 
186     return playlists;
187 }
188 
parseStats(const QByteArray & data)189 MPDStatsValues MPDParseUtils::parseStats(const QByteArray &data)
190 {
191     MPDStatsValues v;
192     QList<QByteArray> lines = data.split('\n');
193 
194     for (const QByteArray &line: lines) {
195         if (line.startsWith(constStatsArtistsKey)) {
196             v.artists=line.mid(constStatsArtistsKey.length()).toUInt();
197         } else if (line.startsWith(constStatsAlbumsKey)) {
198             v.albums=line.mid(constStatsAlbumsKey.length()).toUInt();
199         } else if (line.startsWith(constStatsSongsKey)) {
200             v.songs=line.mid(constStatsSongsKey.length()).toUInt();
201         } else if (line.startsWith(constStatsUptimeKey)) {
202             v.uptime=line.mid(constStatsUptimeKey.length()).toUInt();
203         } else if (line.startsWith(constStatsPlaytimeKey)) {
204             v.playtime=line.mid(constStatsPlaytimeKey.length()).toUInt();
205         } else if (line.startsWith(constStatsDbPlaytimeKey)) {
206             v.dbPlaytime=line.mid(constStatsDbPlaytimeKey.length()).toUInt();
207         } else if (line.startsWith(constStatsDbUpdateKey)) {
208             v.dbUpdate=line.mid(constStatsDbUpdateKey.length()).toUInt();
209         }
210     }
211     return v;
212 }
213 
parseStatus(const QByteArray & data)214 MPDStatusValues MPDParseUtils::parseStatus(const QByteArray &data)
215 {
216     MPDStatusValues v;
217     QList<QByteArray> lines = data.split('\n');
218 
219     for (const QByteArray &line: lines) {
220         if (line.startsWith(constStatusVolumeKey)) {
221             v.volume=line.mid(constStatusVolumeKey.length()).toInt();
222         } else if (line.startsWith(constStatusConsumeKey)) {
223             v.consume=toBool(line.mid(constStatusConsumeKey.length()));
224         } else if (line.startsWith(constStatusRepeatKey)) {
225             v.repeat=toBool(line.mid(constStatusRepeatKey.length()));
226         } else if (line.startsWith(constStatusSingleKey)) {
227             v.single=toBool(line.mid(constStatusSingleKey.length()));
228         } else if (line.startsWith(constStatusRandomKey)) {
229             v.random=toBool(line.mid(constStatusRandomKey.length()));
230         } else if (line.startsWith(constStatusPlaylistKey)) {
231             v.playlist=line.mid(constStatusPlaylistKey.length()).toUInt();
232         } else if (line.startsWith(constStatusPlaylistLengthKey)) {
233             v.playlistLength=line.mid(constStatusPlaylistLengthKey.length()).toInt();
234         } else if (line.startsWith(constStatusCrossfadeKey)) {
235             v.crossFade=line.mid(constStatusCrossfadeKey.length()).toInt();
236         } else if (line.startsWith(constStatusStateKey)) {
237             QByteArray value=line.mid(constStatusStateKey.length());
238             if (constPlayValue==value) {
239                 v.state=MPDState_Playing;
240             } else if (constStopValue==value) {
241                 v.state=MPDState_Stopped;
242             } else {
243                 v.state=MPDState_Paused;
244             }
245         } else if (line.startsWith(constStatusSongKey)) {
246             v.song=line.mid(constStatusSongKey.length()).toInt();
247         } else if (line.startsWith(constStatusSongIdKey)) {
248             v.songId=line.mid(constStatusSongIdKey.length()).toInt();
249         } else if (line.startsWith(constStatusNextSongKey)) {
250             v.nextSong=line.mid(constStatusNextSongKey.length()).toInt();
251         } else if (line.startsWith(constStatusNextSongIdKey)) {
252             v.nextSongId=line.mid(constStatusNextSongIdKey.length()).toInt();
253         } else if (line.startsWith(constStatusTimeKey)) {
254             QList<QByteArray> values=line.mid(constStatusTimeKey.length()).split(':');
255             if (values.length()>1) {
256                 v.timeElapsed=values.at(0).toInt();
257                 v.timeTotal=values.at(1).toInt();
258             }
259         } else if (line.startsWith(constStatusBitrateKey)) {
260             v.bitrate=line.mid(constStatusBitrateKey.length()).toUInt();
261         } else if (line.startsWith(constStatusAudioKey)) {
262             QList<QByteArray> values=line.mid(constStatusAudioKey.length()).split(':');
263             if (3==values.length()) {
264                 v.samplerate=values.at(0).toUInt();
265                 v.bits=values.at(1).toUInt();
266                 v.channels=values.at(2).toUInt();
267             }
268         } else if (line.startsWith(constStatusUpdatingDbKey)) {
269             v.updatingDb=line.mid(constStatusUpdatingDbKey.length()).toInt();
270         } else if (line.startsWith(constStatusErrorKey)) {
271             v.error=QString::fromUtf8(line.mid(constStatusErrorKey.length()));
272             // If we are reporting a stream error, remove any stream name added by Cantata...
273             int start=v.error.indexOf(constHttpProtocol);
274             if (start>0) {
275                 int end=v.error.indexOf(QChar('\"'), start+6);
276                 int pos=v.error.indexOf(QChar('#'), start+6);
277                 if (pos>start && pos<end) {
278                     v.error=v.error.left(pos)+v.error.mid(end);
279                 }
280             }
281         }
282     }
283     return v;
284 }
285 
286 static QSet<QString> constStdProtocols=QSet<QString>() << constHttpProtocol
287                                                        << QLatin1String("https://")
288                                                        << QLatin1String("mms://")
289                                                        << QLatin1String("mmsh://")
290                                                        << QLatin1String("mmst://")
291                                                        << QLatin1String("mmsu://")
292                                                        << QLatin1String("gopher://")
293                                                        << QLatin1String("rtp://")
294                                                        << QLatin1String("rtsp://")
295                                                        << QLatin1String("rtmp://")
296                                                        << QLatin1String("rtmpt://")
297                                                        << QLatin1String("rtmps://");
298 
parseSong(const QList<QByteArray> & lines,Location location)299 Song MPDParseUtils::parseSong(const QList<QByteArray> &lines, Location location)
300 {
301     Song song;
302     for (const QByteArray &line: lines) {
303         if (line.startsWith(constFileKey)) {
304             song.file = QString::fromUtf8(line.mid(constFileKey.length()));
305         } else if (line.startsWith(constTimeKey) ){
306             song.time = line.mid(constTimeKey.length()).toUInt();
307         } else if (line.startsWith(constAlbumKey)) {
308             song.album = QString::fromUtf8(line.mid(constAlbumKey.length()));
309         } else if (line.startsWith(constArtistKey)) {
310             song.artist = QString::fromUtf8(line.mid(constArtistKey.length()));
311         } else if (line.startsWith(constAlbumArtistKey)) {
312             song.albumartist = QString::fromUtf8(line.mid(constAlbumArtistKey.length()));
313         } else if (line.startsWith(constComposerKey)) {
314             song.setComposer(QString::fromUtf8(line.mid(constComposerKey.length())));
315         } else if (line.startsWith(constTitleKey)) {
316             song.title =QString::fromUtf8(line.mid(constTitleKey.length()));
317         } else if (line.startsWith(constTrackKey)) {
318             int v=line.mid(constTrackKey.length()).split('/').at(0).toInt();
319             song.track = v<0 ? 0 : v;
320         } else if (Loc_Library!=location && Loc_Search!=location && line.startsWith(constIdKey)) {
321             song.id = line.mid(constIdKey.length()).toUInt();
322         } else if (line.startsWith(constDiscKey)) {
323             int v = line.mid(constDiscKey.length()).split('/').at(0).toInt();
324             song.disc = v<0 ? 0 : v;
325         } else if (line.startsWith(constDateKey)) {
326             QByteArray value=line.mid(constDateKey.length());
327             int v=value.length()>4 ? value.left(4).toUInt() : value.toUInt();
328             song.year=v<0 ? 0 : v;
329         } else if (line.startsWith(constOriginalDateKey)) {
330             QByteArray value=line.mid(constOriginalDateKey.length());
331             int v=value.length()>4 ? value.left(4).toUInt() : value.toUInt();
332             song.origYear=v<0 ? 0 : v;
333         } else if (line.startsWith(constGenreKey)) {
334             song.addGenre(QString::fromUtf8(line.mid(constGenreKey.length())));
335         } else if (line.startsWith(constNameKey)) {
336             song.setName(QString::fromUtf8(line.mid(constNameKey.length())));
337         } else if (line.startsWith(constPlaylistKey)) {
338             song.file = QString::fromUtf8(line.mid(constPlaylistKey.length()));
339             song.title=Utils::getFile(song.file);
340             song.type=Song::Playlist;
341         }  else if (line.startsWith(constAlbumId)) {
342             song.setMbAlbumId(line.mid(constAlbumId.length()));
343         } else if ((Loc_Search==location || Loc_Library==location) && line.startsWith(constLastModifiedKey)) {
344             song.lastModified=QDateTime::fromString(QString::fromUtf8(line.mid(constLastModifiedKey.length())), Qt::ISODate).toTime_t();
345         } else if ((Loc_Search==location || Loc_Playlists==location || Loc_PlayQueue==location) && line.startsWith(constPerformerKey)) {
346             if (song.hasPerformer()) {
347                 song.setPerformer(song.performer()+QLatin1String(", ")+QString::fromUtf8(line.mid(constPerformerKey.length())));
348             } else {
349                 song.setPerformer(QString::fromUtf8(line.mid(constPerformerKey.length())));
350             }
351         } else if (Loc_PlayQueue==location) {
352             if (line.startsWith(constPriorityKey)) {
353                 song.priority = line.mid(constPriorityKey.length()).toUInt();
354             } else if (line.startsWith(constCommentKey)) {
355                 song.setComment(QString::fromUtf8(line.mid(constCommentKey.length())));
356             }
357         } else if (Loc_Library==location) {
358             if (line.startsWith(constAlbumSortKey)) {
359                 song.setAlbumSort(QString::fromUtf8(line.mid(constAlbumSortKey.length())));
360             } else if (line.startsWith(constArtistSortKey)) {
361                 song.setArtistSort(QString::fromUtf8(line.mid(constArtistSortKey.length())));
362             } else if (line.startsWith(constAlbumArtistSortKey)) {
363                 song.setAlbumArtistSort(QString::fromUtf8(line.mid(constAlbumArtistSortKey.length())));
364             }
365         }
366     }
367 
368     if (Song::Playlist!=song.type && song.genres[0].isEmpty()) {
369         song.addGenre(Song::unknown());
370     }
371 
372     if (Loc_Library==location) {
373         song.guessTags();
374         song.fillEmptyFields();
375     } else if (Loc_Streams==location) {
376         song.setName(getAndRemoveStreamName(song.file, true));
377     } else {
378         QString origFile=song.file;
379         bool modifiedFile=false;
380 
381         #ifdef ENABLE_HTTP_SERVER
382         if (!song.file.isEmpty() && song.file.startsWith(constHttpProtocol) && HttpServer::self()->isOurs(song.file)) {
383             song.type=Song::CantataStream;
384             Song mod=HttpServer::self()->decodeUrl(song.file);
385             if (!mod.title.isEmpty()) {
386                 mod.id=song.id;
387                 mod.priority=song.priority;
388                 song=mod;
389             } else {
390                 song.file=mod.file;
391                 song.time=mod.time;
392             }
393             song.setLocalPath(mod.file);
394             modifiedFile=true;
395         } else
396         #endif
397             if (song.file.contains(Song::constCddaProtocol)) {
398                 song.type=Song::Cdda;
399             } else if (song.file.contains(constProtocol)) {
400                 for (const QString &protocol: constStdProtocols) {
401                     if (song.file.startsWith(protocol)) {
402                         song.type=Song::Stream;
403                         break;
404                     }
405                 }
406             }
407 
408         if (!song.file.isEmpty()) {
409             if (song.isStream()) {
410                 if (song.isCantataStream()) {
411                     if (song.title.isEmpty()) {
412                         QStringList parts=QUrl(song.file).path().split('/');
413                         if (!parts.isEmpty()) {
414                             song.title=parts.last();
415                             song.fillEmptyFields();
416                         }
417                     }
418                 } else {
419                     if (OnlineService::decode(song)) {
420                         modifiedFile=true;
421                     } else {
422                         QString name=getAndRemoveStreamName(song.file);
423                         if (!name.isEmpty()) {
424                             song.setName(name);
425                         }
426                         if (song.title.isEmpty() && song.name().isEmpty()) {
427                             song.title=Utils::getFile(QUrl(song.file).path());
428                         }
429                     }
430                 }
431             } else if (Loc_PlayQueue==location && Song::Standard==song.type && !singleTracksFolders.isEmpty() && singleTracksFolders.contains(Utils::getDir(song.file, false))) {
432                 song.setFromSingleTracks();
433                 song.fillEmptyFields();
434             } else {
435                 song.guessTags();
436                 song.fillEmptyFields();
437             }
438         }
439         // HTTP server, and OnlineServices, modify the path. But this then messes up
440         // undo/restore of playqueue. Therefore, set path back to original value...
441         if (modifiedFile) {
442             song.setDecodedPath(song.file);
443         }
444         song.file=origFile;
445         song.setKey(location);
446 
447         // Check for downloaded podcasts played via local file playback
448         if (Song::OnlineSvrTrack!=song.type && PodcastService::isPodcastFile(song.file)) {
449             song.setIsFromOnlineService(PodcastService::constName);
450             song.albumartist=song.artist=PodcastService::constName;
451         }
452     }
453     return song;
454 }
455 
parseSongs(const QByteArray & data,Location location)456 QList<Song> MPDParseUtils::parseSongs(const QByteArray &data, Location location)
457 {
458     QList<Song> songs;
459     QList<QByteArray> currentItem;
460     QList<QByteArray> lines = data.split('\n');
461     int amountOfLines = lines.size();
462 
463     for (int i = 0; i < amountOfLines; i++) {
464         const QByteArray &line=lines.at(i);
465         // Skip the "OK" line, this is NOT a song!!!
466         if (constOkValue==line) {
467             continue;
468         }
469         if (!line.isEmpty()) {
470             currentItem.append(line);
471         }
472         if (i == lines.size() - 1 || lines.at(i + 1).startsWith(constFileKey)) {
473             Song song=parseSong(currentItem, location);
474             if (!song.file.isEmpty()) {
475                 songs.append(song);
476             }
477             currentItem.clear();
478         }
479     }
480 
481     return songs;
482 }
483 
parseChanges(const QByteArray & data)484 QList<MPDParseUtils::IdPos> MPDParseUtils::parseChanges(const QByteArray &data)
485 {
486     QList<IdPos> changes;
487     QList<QByteArray> lines = data.split('\n');
488     int amountOfLines = lines.size();
489     quint32 cpos=0;
490     bool foundCpos=false;
491 
492     for (int i = 0; i < amountOfLines; i++) {
493         const QByteArray &line = lines.at(i);
494         // Skip the "OK" line, this is NOT a song!!!
495         if (constOkValue==line || line.length()<1) {
496             continue;
497         }
498         QList<QByteArray> tokens = line.split(':');
499         if (2!=tokens.count()) {
500             return QList<IdPos>();
501         }
502         QByteArray element = tokens.takeFirst();
503         QByteArray value = tokens.takeFirst();
504         if (constChangePosKey==element) {
505             if (foundCpos) {
506                 return QList<IdPos>();
507             }
508             foundCpos=true;
509             cpos = value.toInt();
510         } else if (constChangeIdKey==element) {
511             if (!foundCpos) {
512                 return QList<IdPos>();
513             }
514             foundCpos=false;
515             qint32 id=value.toInt();
516             changes.append(IdPos(id, cpos));
517         }
518     }
519 
520     return changes;
521 }
522 
parseList(const QByteArray & data,const QByteArray & key)523 QStringList MPDParseUtils::parseList(const QByteArray &data, const QByteArray &key)
524 {
525     QStringList items;
526     QList<QByteArray> lines = data.split('\n');
527     int keyLen=key.length();
528 
529     for (const QByteArray &line: lines) {
530         // Skip the "OK" line, this is NOT a valid item!!!
531         if (constOkValue==line) {
532             continue;
533         }
534         if (line.startsWith(key)) {
535             items.append(QString::fromUtf8(line.mid(keyLen).replace("://", "")));
536         }
537     }
538 
539     return items;
540 }
541 
parseMessages(const QByteArray & data)542 MPDParseUtils::MessageMap MPDParseUtils::parseMessages(const QByteArray &data)
543 {
544     MPDParseUtils::MessageMap messages;
545     QList<QByteArray> lines = data.split('\n');
546     QByteArray channel;
547 
548     for (const QByteArray &line: lines) {
549         // Skip the "OK" line, this is NOT a valid item!!!
550         if (constOkValue==line) {
551             continue;
552         }
553         if (line.startsWith(constChannel)) {
554             channel=line.mid(constChannel.length());
555         } else if (line.startsWith(constMessage)) {
556             messages[channel].append(QString::fromUtf8(line.mid(constMessage.length())));
557         }
558     }
559     return messages;
560 }
561 
parseDirItems(const QByteArray & data,const QString & mpdDir,long mpdVersion,QList<Song> & songList,const QString & dir,QStringList & subDirs,Location loc)562 void MPDParseUtils::parseDirItems(const QByteArray &data, const QString &mpdDir, long mpdVersion, QList<Song> &songList, const QString &dir, QStringList &subDirs, Location loc)
563 {
564     QList<QByteArray> currentItem;
565     QList<QByteArray> lines = data.split('\n');
566     int amountOfLines = lines.size();
567     bool parsePlaylists="/"!=dir && ""!=dir;
568     bool setSingleTracks=parsePlaylists && singleTracksFolders.contains(dir) && Loc_Browse!=loc;
569     QList<Song> songs;
570 
571     for (int i = 0; i < amountOfLines; i++) {
572         const QByteArray &line=lines.at(i);
573         if (line.startsWith(constDirectoryKey)) {
574             subDirs.append(QString::fromUtf8(line.mid(constDirectoryKey.length())));
575         }
576         currentItem.append(line);
577         if (i == amountOfLines - 1 || lines.at(i + 1).startsWith(constFileKey) || lines.at(i + 1).startsWith(constPlaylistKey)) {
578             Song currentSong = parseSong(currentItem, Loc_Library);
579             currentItem.clear();
580             if (currentSong.file.isEmpty()) {
581                 continue;
582             }
583 
584             DBUG << currentSong.file;
585             if (Song::Playlist==currentSong.type) {
586                 // lsinfo will return all stored playlists - but this is deprecated.
587                 if (!parsePlaylists) {
588                     continue;
589                 }
590 
591                 if (!currentSong.isCueFile()) {
592                     // In Folders/Browse, we can list all playlists
593                     if (Loc_Browse==loc) {
594                         songs.append(currentSong);
595                     }
596                     // Only add CUE files to library listing...
597                     continue;
598                 }
599 
600                 switch (cueSupport) {
601                 case Cue_Ignore:
602                     continue;
603                     break;
604                 case Cue_Parse:
605                     if (Loc_Browse==loc) {
606                         songs.append(currentSong);
607                     }
608                     if (Loc_Library!=loc) {
609                         continue;
610                     }
611                     break;
612                 case Cue_ListButDontParse:
613                     if (Loc_Browse==loc) {
614                         songs.append(currentSong);
615                     }
616                 default:
617                     continue;
618                     break;
619                 }
620 
621                 // No source files for CUE file..
622                 if (songs.isEmpty()) {
623                     continue;
624                 }
625 
626                 Song firstSong=songs.at(0);
627                 QList<Song> cueSongs; // List of songs from cue file
628                 QSet<QString> cueFiles; // List of source (flac, mp3, etc) files referenced in cue file
629 
630                 DBUG << "Got playlist item" << currentSong.file;
631 
632                 bool canSplitCue=mpdVersion>=CANTATA_MAKE_VERSION(0,17,0);
633                 bool parseCue=canSplitCue && currentSong.isCueFile() && !mpdDir.startsWith(constHttpProtocol) && QFile::exists(mpdDir+currentSong.file);
634                 bool cueParseStatus=false;
635                 double lastTrackIndex=0.0;
636                 if (parseCue) {
637                     DBUG << "Parsing cue file:" << currentSong.file << "mpdDir:" << mpdDir;
638                     cueParseStatus=CueFile::parse(currentSong.file, mpdDir, cueSongs, cueFiles, lastTrackIndex);
639                     if (!cueParseStatus) {
640                         DBUG << "Failed to parse cue file!";
641                         continue;
642                     } else DBUG << "Parsed cue file, songs:" << cueSongs.count() << "files:" << cueFiles;
643                 }
644                 if (cueParseStatus && cueSongs.count()>=songs.count() &&
645                         (cueFiles.count()<cueSongs.count() || (firstSong.albumArtist().isEmpty() && firstSong.album.isEmpty()))) {
646 
647                     bool canUseThisCueFile=true;
648                     for (const Song &s: cueSongs) {
649                         if (!QFile::exists(mpdDir+s.name())) {
650                             DBUG << QString(mpdDir+s.name()) << "is referenced in cue file, but does not exist in MPD folder";
651                             canUseThisCueFile=false;
652                             break;
653                         }
654                     }
655 
656                     if (!canUseThisCueFile) {
657                         continue;
658                     }
659 
660                     bool canUseCueFileTracks=false;
661                     QList<Song> fixedCueSongs; // Songs taken from cueSongs that have been updated...
662 
663                     if (songs.size()==cueFiles.size()) {
664                         quint32 albumTime=0;
665                         QMap<QString, Song> origFiles;
666                         for (const Song &s: songs) {
667                             origFiles.insert(s.file, s);
668                             albumTime+=s.time;
669                         }
670                         DBUG << "Original files:" << origFiles.keys();
671 
672                         bool setTimeFromSource=origFiles.size()==cueSongs.size();
673                         DBUG << "setTimeFromSource" << setTimeFromSource << "at" << albumTime << "#c" << cueFiles.size();
674                         for (const Song &orig: cueSongs) {
675                             Song s=orig;
676                             Song albumSong=origFiles[s.name()];
677                             s.setName(QString()); // CueFile has placed source file name here!
678                             if (s.artist.isEmpty() && !albumSong.artist.isEmpty()) {
679                                 s.artist=albumSong.artist;
680                                 DBUG << "Get artist from album" << albumSong.artist;
681                             }
682                             if (s.composer().isEmpty() && !albumSong.composer().isEmpty()) {
683                                 s.setComposer(albumSong.composer());
684                                 DBUG << "Get composer from album" << albumSong.composer();
685                             }
686                             if (s.album.isEmpty() && !albumSong.album.isEmpty()) {
687                                 s.album=albumSong.album;
688                                 DBUG << "Get album from album" << albumSong.album;
689                             }
690                             if (s.albumartist.isEmpty() && !albumSong.albumartist.isEmpty()) {
691                                 s.albumartist=albumSong.albumartist;
692                                 DBUG << "Get albumartist from album" << albumSong.albumartist;
693                             }
694                             if (0==s.year && 0!=albumSong.year) {
695                                 s.year=albumSong.year;
696                                 DBUG << "Get year from album" << albumSong.year;
697                             }
698                             if (0==s.time && setTimeFromSource) {
699                                 s.time=albumSong.time;
700                             } else if (0==s.time && 1==cueFiles.size()) {
701                                 DBUG << "Set time of last track" << s.title << s.time << albumTime << (lastTrackIndex/1000.0);
702                                 // Try to set duration of last track by subtracting previous track durations from album duration...
703                                 s.time=albumTime-(lastTrackIndex/1000.0);
704                             }
705                             DBUG << s.title << s.time;
706                             fixedCueSongs.append(s);
707                         }
708                         canUseCueFileTracks=true;
709                     } else DBUG << "ERROR: file count mismatch" << songs.size() << cueFiles.size();
710 
711                     if (!canUseCueFileTracks) {
712                         // Album had a different number of source files to the CUE file. If so, then we need to ensure
713                         // all tracks have meta data - otherwise just fallback to listing file + cue
714                         for (const Song &orig: cueSongs) {
715                             Song s=orig;
716                             s.setName(QString()); // CueFile has placed source file name here!
717                             if (s.artist.isEmpty() || s.album.isEmpty()) {
718                                 break;
719                             }
720                             fixedCueSongs.append(s);
721                         }
722 
723                         if (fixedCueSongs.count()==cueSongs.count()) {
724                             canUseCueFileTracks=true;
725                         } else DBUG << "ERROR: Not all cue tracks had meta data";
726                     }
727 
728                     if (canUseCueFileTracks) {
729                         songs = fixedCueSongs;
730                     }
731                     continue;
732                 }
733 
734                 if (!firstSong.albumArtist().isEmpty() && !firstSong.album.isEmpty()) {
735                     currentSong.albumartist=firstSong.albumArtist();
736                     currentSong.album=firstSong.album;
737                     songs.append(currentSong);
738                 }
739             } else {
740                 if (setSingleTracks) {
741                     currentSong.setFromSingleTracks();
742                 }
743                 currentSong.fillEmptyFields();
744                 songs.append(currentSong);
745             }
746         }
747     }
748     if (Loc_Browse==loc) {
749         QList<Song> sngs;
750         QList<Song> playlists;
751         for (const auto &s: songs) {
752             if (Song::Playlist==s.type) {
753                 playlists.append(s);
754             } else {
755                 sngs.append(s);
756             }
757         }
758         std::sort(playlists.begin(), playlists.end());
759         songs=sngs;
760         songs+=playlists;
761     }
762     songList+=songs;
763 }
764 
parseOuputs(const QByteArray & data)765 QList<Output> MPDParseUtils::parseOuputs(const QByteArray &data)
766 {
767     QList<Output> outputs;
768     QList<QByteArray> lines = data.split('\n');
769     Output output;
770 
771     for (const QByteArray &line: lines) {
772         if (constOkValue==line) {
773             break;
774         }
775 
776         if (line.startsWith(constOutputIdKey)) {
777             if (!output.name.isEmpty()) {
778                 outputs << output;
779                 output.name=QString();
780             }
781             output.id=line.mid(constOutputIdKey.length()).toUInt();
782         } else if (line.startsWith(constOutputNameKey)) {
783             output.name=line.mid(constOutputNameKey.length());
784         } else if (line.startsWith(constOutputEnabledKey)) {
785             output.enabled=toBool(line.mid(constOutputEnabledKey.length()));
786         }
787     }
788 
789     if (!output.name.isEmpty()) {
790         outputs << output;
791     }
792 
793     return outputs;
794 }
795 
796 static const QByteArray constSticker("sticker: ");
parseSticker(const QByteArray & data,const QByteArray & sticker)797 QByteArray MPDParseUtils::parseSticker(const QByteArray &data, const QByteArray &sticker)
798 {
799     QList<QByteArray> lines = data.split('\n');
800     QByteArray key=constSticker+sticker+'=';
801     for (const QByteArray &line: lines) {
802         if (line.startsWith(key)) {
803             return line.mid(key.length());
804         }
805     }
806     return QByteArray();
807 }
808 
parseStickers(const QByteArray & data,const QByteArray & sticker)809 QList<MPDParseUtils::Sticker> MPDParseUtils::parseStickers(const QByteArray &data, const QByteArray &sticker)
810 {
811     QList<Sticker> stickers;
812     QList<QByteArray> lines = data.split('\n');
813     Sticker s;
814     QByteArray key=constSticker+sticker+'=';
815 
816     for (const QByteArray &line: lines) {
817         if (constOkValue==line) {
818             break;
819         }
820 
821         if (line.startsWith(constFileKey)) {
822             s.file=line.mid(constFileKey.length());
823         } else if (line.startsWith(key)) {
824             s.value=line.mid(key.length());
825             stickers.append(s);
826         }
827     }
828 
829     return stickers;
830 }
831 
832 static const QString constStreamName("StreamName");
833 
addStreamName(const QString & url,const QString & name,bool singleHash)834 QString MPDParseUtils::addStreamName(const QString &url, const QString &name, bool singleHash)
835 {
836     return Utils::addHashParam(url, singleHash ? QString() : constStreamName, name);
837 }
838 
getStreamName(const QString & url)839 QString MPDParseUtils::getStreamName(const QString &url)
840 {
841     DBUG << url;
842     QMap<QString, QString> kv = Utils::hashParams(url);
843     QMap<QString, QString>::ConstIterator name = kv.constFind(constStreamName);
844     if (kv.constEnd()!=name) {
845         DBUG << "name" << name.value();
846         return name.value();
847     }
848     return QString();
849 }
850 
getAndRemoveStreamName(QString & url,bool checkSingleHash)851 QString MPDParseUtils::getAndRemoveStreamName(QString &url, bool checkSingleHash)
852 {
853     DBUG << url;
854     QMap<QString, QString> kv = Utils::hashParams(url);
855     QMap<QString, QString>::ConstIterator name = kv.constFind(constStreamName);
856     if (kv.constEnd()==name && checkSingleHash) {
857         DBUG << "check single";
858         name = kv.find("-");
859     }
860     if (kv.constEnd()==name) {
861         DBUG << "no name found";
862         return QString();
863     }
864     url=Utils::removeHash(url);
865     DBUG << "name" << name.value();
866     return name.value();
867 }
868