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