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 <cmath>
28 #include "config.h"
29 #include "song.h"
30 #if !defined CANTATA_NO_UI_FUNCTIONS
31 #include "online/onlineservice.h"
32 #include "online/podcastservice.h"
33 #endif
34 #include <QStringList>
35 #include <QSet>
36 #include <QChar>
37 #include <QLatin1Char>
38 #include <QtAlgorithms>
39 #include <QUrl>
40 #include <QMutex>
41 #include <QMutexLocker>
42 #include <algorithm>
43 
44 //static const quint8 constOnlineDiscId=0xEE;
45 
46 const QString Song::constCddaProtocol=QLatin1String("/[cantata-cdda]/");
47 const QString Song::constMopidyLocal=QLatin1String("local:track:");
48 const QString Song::constForkedDaapdLocal=QLatin1String("file:");
49 
50 static QString unknownStr;
51 static QString variousArtistsStr;
52 static QString singleTracksStr;
53 static bool useOrigYear = false;
54 
unknown()55 const QString & Song::unknown()
56 {
57     return unknownStr;
58 }
59 
variousArtists()60 const QString & Song::variousArtists()
61 {
62     return variousArtistsStr;
63 }
64 
singleTracks()65 const QString & Song::singleTracks()
66 {
67     return singleTracksStr;
68 }
69 
initTranslations()70 void Song::initTranslations()
71 {
72     unknownStr=QObject::tr("Unknown");
73     variousArtistsStr=QObject::tr("Various Artists");
74     singleTracksStr=QObject::tr("Single Tracks");
75 }
76 
77 // When displaying albums, we use the 1st track's year as the year of the album.
78 // The map below stores the mapping from artist+album to year.
79 // This way the grouped view can find this quickly...
80 static QHash<QString, quint16> albumYears;
81 
storeAlbumYear(const Song & s)82 void Song::storeAlbumYear(const Song &s)
83 {
84     albumYears.insert(s.albumKey(), s.displayYear());
85 }
86 
albumYear(const Song & s)87 int Song::albumYear(const Song &s)
88 {
89     QHash<QString, quint16>::ConstIterator it=albumYears.find(s.albumKey());
90     return it==albumYears.end() ? s.displayYear() : it.value();
91 }
92 
songType(const Song & s)93 static int songType(const Song &s)
94 {
95     static QStringList extensions=QStringList() << QLatin1String(".flac")
96                                                 << QLatin1String(".wav")
97                                                 << QLatin1String(".dff")
98                                                 << QLatin1String(".dsf")
99                                                 << QLatin1String(".aac")
100                                                 << QLatin1String(".m4a")
101                                                 << QLatin1String(".m4b")
102                                                 << QLatin1String(".m4p")
103                                                 << QLatin1String(".mp4")
104                                                 << QLatin1String(".ogg")
105                                                 << QLatin1String(".opus")
106                                                 << QLatin1String(".mp3")
107                                                 << QLatin1String(".wma");
108 
109     for (int i=0; i<extensions.count(); ++i) {
110         if (s.file.endsWith(extensions.at(i), Qt::CaseInsensitive)) {
111             return i;
112         }
113     }
114 
115     if (s.isCdda()) {
116         return extensions.count()+2;
117     }
118     if (s.isStream()) {
119         return extensions.count()+3;
120     }
121     return extensions.count()+1;
122 }
123 
songTypeSort(const Song & s1,const Song & s2)124 static bool songTypeSort(const Song &s1, const Song &s2)
125 {
126     int t1=songType(s1);
127     int t2=songType(s2);
128     return t1<t2 || (t1==t2 && s1.id<s2.id);
129 }
130 
sortViaType(QList<Song> & songs)131 void Song::sortViaType(QList<Song> &songs)
132 {
133     std::sort(songs.begin(), songs.end(), songTypeSort);
134 }
135 
decodePath(const QString & file,bool cdda)136 QString Song::decodePath(const QString &file, bool cdda)
137 {
138     if (cdda) {
139         return QString(file).replace("/", "_").replace(":", "_");
140     }
141     if (file.startsWith(constMopidyLocal)) {
142         return QUrl::fromPercentEncoding(file.mid(constMopidyLocal.length()).toLatin1());
143     }
144     if (file.startsWith(constForkedDaapdLocal)) {
145         return file.mid(constForkedDaapdLocal.length());
146     }
147     return file;
148 }
149 
encodePath(const QString & file)150 QString Song::encodePath(const QString &file)
151 {
152     return constMopidyLocal+QString(QUrl::toPercentEncoding(file, "/"));
153 }
154 
155 static QSet<QString> compGenres;
156 
composerGenres()157 const QSet<QString> &Song::composerGenres()
158 {
159     return compGenres;
160 }
161 
setComposerGenres(const QSet<QString> & g)162 void Song::setComposerGenres(const QSet<QString> &g)
163 {
164     compGenres=g;
165 }
166 
Song()167 Song::Song()
168     : extraFields(0)
169     , priority(0)
170     , disc(0)
171     , blank(0)
172     , time(0)
173     , track(0)
174     , origYear(0)
175     , year(0)
176     , type(Standard)
177     , guessed(false)
178     , id(-1)
179     , size(0)
180     , rating(Rating_Null)
181     , lastModified(0)
182     , key(Null_Key)
183 {
184 }
185 
operator =(const Song & s)186 Song & Song::operator=(const Song &s)
187 {
188     id = s.id;
189     file = s.file;
190     time = s.time;
191     album = s.album;
192     artist = s.artist;
193     albumartist = s.albumartist;
194     title = s.title;
195     track = s.track;
196 //     pos = s.pos;
197     disc = s.disc;
198     blank = s.blank;
199     priority = s.priority;
200     origYear = s.origYear;
201     year = s.year;
202     for (int i=0; i<constNumGenres; ++i) {
203         genres[i]=s.genres[i];
204     }
205     size = s.size;
206     rating = s.rating;
207     key = s.key;
208     type = s.type;
209     guessed = s.guessed;
210     extra = s.extra;
211     extraFields = s.extraFields;
212     lastModified = s.lastModified;
213     return *this;
214 }
215 
operator ==(const Song & o) const216 bool Song::operator==(const Song &o) const
217 {
218     return 0==compareTo(o);
219 }
220 
operator <(const Song & o) const221 bool Song::operator<(const Song &o) const
222 {
223     return compareTo(o)<0;
224 }
225 
compareTo(const Song & o) const226 int Song::compareTo(const Song &o) const
227 {
228     //
229     // NOTE: This compare DOES NOT use XXXSORT tags
230     //
231 
232     if (type!=o.type) {
233         return type<o.type ? -1 : 1;
234     }
235 
236     // For playlists, we only need to compare filename...
237     if (Playlist!=type) {
238         if (SingleTracks==type) {
239             int compare=artistSong().localeAwareCompare(artistSong());
240             if (0!=compare) {
241                 return compare<0;
242             }
243         }
244 
245         int compare=albumArtistOrComposer().localeAwareCompare(o.albumArtistOrComposer());
246         if (0!=compare) {
247             return compare;
248         }
249         compare=album.localeAwareCompare(o.album);
250         if (0!=compare) {
251             return compare;
252         }
253         compare=mbAlbumId().compare(o.mbAlbumId());
254         if (0!=compare) {
255             return compare;
256         }
257         if (disc!=o.disc) {
258             return disc<o.disc ? -1 : 1;
259         }
260         if (track!=o.track) {
261             return track<o.track ? -1 : 1;
262         }
263         if (useOrigYear) {
264             if (origYear!=o.origYear) {
265                 return origYear<o.origYear ? -1 : 1;
266             }
267             if (year!=o.year) {
268                 return year<o.year ? -1 : 1;
269             }
270         } else {
271             if (year!=o.year) {
272                 return year<o.year ? -1 : 1;
273             }
274             if (origYear!=o.origYear) {
275                 return origYear<o.origYear ? -1 : 1;
276             }
277         }
278         compare=title.localeAwareCompare(o.title);
279         if (0!=compare) {
280             return compare;
281         }
282         compare=name().compare(o.name());
283         if (0!=compare) {
284             return compare;
285         }
286         compare=compareGenres(o);
287         if (0!=compare) {
288             return compare;
289         }
290         if (time!=o.time) {
291             return time<o.time ? -1 : 1;
292         }
293     }
294     return file.compare(o.file);
295 }
296 
isEmpty() const297 bool Song::isEmpty() const
298 {
299     return (artist.isEmpty() && album.isEmpty() && title.isEmpty() && name().isEmpty()) || file.isEmpty();
300 }
301 
sameMetadata(const Song & o) const302 bool Song::sameMetadata(const Song &o) const
303 {
304     return disc == o.disc && track == o.track && time == o.time && year == o.year && origYear == o.origYear &&
305            artist == o.artist && albumartist == o.albumartist && album == o.album && title == o.title;
306 }
307 
guessTags()308 void Song::guessTags()
309 {
310     if (isEmpty() && !isStream()) {
311         static const QLatin1String constAlbumArtistSep(" - ");
312         guessed=true;
313         QStringList parts = file.split("/", QString::SkipEmptyParts);
314         if (3==parts.length()) {
315             title=parts.at(2);
316             album=parts.at(1);
317             artist=parts.at(0);
318         } else if (2==parts.length() && parts.at(0).contains(constAlbumArtistSep)) {
319             title=parts.at(1);
320             QStringList albumArtistParts = parts.at(0).split(constAlbumArtistSep, QString::SkipEmptyParts);
321             if (2==albumArtistParts.length()) {
322                 album=albumArtistParts.at(1);
323                 artist=albumArtistParts.at(0);
324             }
325         } else if (!parts.isEmpty()) {
326             title=parts.at(parts.length()-1);
327         }
328 
329         if (!title.isEmpty()) {
330             int dot=title.lastIndexOf('.');
331             if (dot>0 && dot<title.length()-2) {
332                 title=title.left(dot);
333             }
334             static const QSet<QChar> constSeparators=QSet<QChar>() << QLatin1Char(' ') << QLatin1Char('-') << QLatin1Char('_') << QLatin1Char('.');
335             int separator=0;
336 
337             for (const QChar &sep: constSeparators) {
338                 separator=title.indexOf(sep);
339                 if (1==separator || 2==separator) {
340                     break;
341                 }
342             }
343 
344             if ( (1==separator && title[separator-1].isDigit()) ||
345                  (2==separator && title[separator-2].isDigit() && title[separator-1].isDigit()) ) {
346                 if (0==track) {
347                     track=title.left(separator).toInt();
348                 }
349                 title=title.mid(separator+1);
350 
351                 while (!title.isEmpty() && constSeparators.contains(title[0])) {
352                     title=title.mid(1);
353                 }
354             }
355         }
356     }
357 }
358 
revertGuessedTags()359 void Song::revertGuessedTags()
360 {
361     title=artist=album=unknownStr;
362 }
363 
fillEmptyFields()364 void Song::fillEmptyFields()
365 {
366     if (artist.isEmpty()) {
367         artist = unknownStr;
368         blank |= BlankArtist;
369     }
370     if (album.isEmpty()) {
371         album = unknownStr;
372         blank |= BlankAlbum;
373     }
374     if (title.isEmpty()) {
375         title = unknownStr;
376         blank |= BlankTitle;
377     }
378     if (genres[0].isEmpty()) {
379         genres[0]=unknownStr;
380     }
381 }
382 
383 struct KeyStore
384 {
KeyStoreKeyStore385     KeyStore() : currentKey(0) { }
386     quint16 currentKey;
387     QHash<QString, quint16> keys;
388 };
389 
390 static QHash<int, KeyStore> storeMap;
391 static QMutex storeMapMutex;
392 
clearKeyStore(int location)393 void Song::clearKeyStore(int location)
394 {
395     QMutexLocker locker(&storeMapMutex);
396     storeMap.remove(location);
397 }
398 
displayAlbum(const QString & albumName,quint16 albumYear)399 QString Song::displayAlbum(const QString &albumName, quint16 albumYear)
400 {
401     return albumYear>0 ? albumName+QLatin1String(" (")+QString::number(albumYear)+QLatin1Char(')') : albumName;
402 }
403 
404 static QSet<QString> prefixesToIngore=QSet<QString>() << QLatin1String("The");
405 
ignorePrefixes()406 QSet<QString> Song::ignorePrefixes()
407 {
408     return prefixesToIngore;
409 }
410 
setIgnorePrefixes(const QSet<QString> & prefixes)411 void Song::setIgnorePrefixes(const QSet<QString> &prefixes)
412 {
413     prefixesToIngore=prefixes;
414 }
415 
ignorePrefix(const QString & str)416 static QString ignorePrefix(const QString &str)
417 {
418     for (const QString &p: prefixesToIngore) {
419         if (str.startsWith(p+QLatin1Char(' '))) {
420             return str.mid(p.length()+1)+QLatin1String(", ")+p;
421         }
422     }
423     return QString();
424 }
425 
sortString(const QString & str)426 QString Song::sortString(const QString &str)
427 {
428     QString sort=ignorePrefix(str);
429 
430     if (sort.isEmpty()) {
431         sort=str;
432     }
433     sort=sort.remove('.');
434     return sort==str ? QString() : sort;
435 }
436 
useOriginalYear()437 bool Song::useOriginalYear()
438 {
439     return useOrigYear;
440 }
441 
setUseOriginalYear(bool u)442 void Song::setUseOriginalYear(bool u)
443 {
444     useOrigYear = u;
445 }
446 
setKey(int location)447 quint16 Song::setKey(int location)
448 {
449     if (isStandardStream()) {
450         key=0;
451         return 0;
452     }
453 
454     QString songKey(albumKey());
455     QMutexLocker locker(&storeMapMutex);
456     KeyStore &store=storeMap[location];
457     QHash<QString, quint16>::ConstIterator it=store.keys.find(songKey);
458 
459     if (it!=store.keys.end()) {
460         key=it.value();
461     } else {
462         store.currentKey++; // Key 0 is for streams, so we need to increment before setting...
463         store.keys.insert(songKey, store.currentKey);
464         key=store.currentKey;
465     }
466     return key;
467 }
468 
isUnknownAlbum() const469 bool Song::isUnknownAlbum() const
470 {
471     return (album.isEmpty() || album==unknownStr) && (albumArtist().isEmpty() || albumArtist()==unknownStr);
472 }
473 
isInvalid() const474 bool Song::isInvalid() const
475 {
476     return 0==time && guessed && !file.contains("://") && (genres[0].isEmpty() || genres[0]==unknownStr) && name().isEmpty();
477 }
478 
clear()479 void Song::clear()
480 {
481     id = -1;
482     file.clear();
483     time = 0;
484     album.clear();
485     artist.clear();
486     title.clear();
487     track = 0;
488 //     pos = 0;
489     disc = 0;
490     blank = 0;
491     year = 0;
492     origYear = 0;
493     for (int i=0; i<constNumGenres; ++i) {
494         genres[i]=QString();
495     }
496     size = 0;
497     extra.clear();
498     type = Standard;
499 }
500 
501 const QLatin1Char Song::constFieldSep('\001');
502 const QString Song::constSep=QString::fromUtf8(" \u2022 ");
503 
addGenre(const QString & g)504 void Song::addGenre(const QString &g)
505 {
506     for (int i=0; i<constNumGenres; ++i) {
507         if (genres[i].isEmpty()) {
508             genres[i]=g;
509             break;
510         }
511     }
512 }
513 
displayYear() const514 quint16 Song::displayYear() const
515 {
516     return useOrigYear && origYear>0 ? origYear : year;
517 }
518 
entryName() const519 QString Song::entryName() const
520 {
521     if (title.isEmpty()) {
522         return file;
523     }
524 
525     return title+constFieldSep+artist+constFieldSep+album;
526 }
527 
albumArtistOrComposer() const528 QString Song::albumArtistOrComposer() const
529 {
530     if (useComposer()) {
531         QString c=composer();
532         if (!c.isEmpty()) {
533             return c;
534         }
535     }
536     return albumArtist();
537 }
538 
trackArtistOrComposer() const539 QString Song::trackArtistOrComposer() const
540 {
541     if (useComposer()) {
542         QString c=composer();
543         if (!c.isEmpty()) {
544             return c;
545         }
546     }
547     return artist;
548 }
549 
albumName() const550 QString Song::albumName() const
551 {
552     if (useComposer()) {
553         QString c=composer();
554         if (!c.isEmpty() && c!=albumArtist()) {
555             return album+QLatin1String(" (")+albumArtist()+QLatin1Char(')');
556         }
557     }
558     return album;
559 }
560 
albumId() const561 QString Song::albumId() const
562 {
563     QString mb=mbAlbumId();
564     return mb.isEmpty() ? album : mb;
565 }
566 
artistSong() const567 QString Song::artistSong() const
568 {
569     //return artist+constSep+title;
570     return title+constSep+artist;
571 }
572 
trackAndTitleStr(bool showArtistIfDifferent) const573 QString Song::trackAndTitleStr(bool showArtistIfDifferent) const
574 {
575     #if !defined CANTATA_NO_UI_FUNCTIONS
576     if ((OnlineSvrTrack==type || Song::CantataStream) && OnlineService::showLogoAsCover(*this)) {
577         return artistSong();
578     }
579     #endif
580 //    if (isFromOnlineService()) {
581 //        return (disc>0 && disc!=constOnlineDiscId ? (QString::number(disc)+QLatin1Char('.')) : QString())+
582 //               (track>0 ? (track>9 ? QString::number(track) : (QLatin1Char('0')+QString::number(track))) : QString())+
583 //               QLatin1Char(' ')+(addArtist ? artistSong() : title);
584 //    }
585     return //(disc>0 ? (QString::number(disc)+QLatin1Char('.')) : QString())+
586            (track>0 && SingleTracks!=type ? (track>9 ? QString::number(track)+QLatin1Char(' ') : (QLatin1Char('0')+QString::number(track)+QLatin1Char(' '))) : QString())+
587            (showArtistIfDifferent && diffArtist() ? artistSong() : title) +
588            (origYear>0 && origYear != year ? QLatin1String(" (")+QString::number(origYear)+QLatin1Char(')') : QString());
589 }
590 
591 #ifndef CANTATA_NO_UI_FUNCTIONS
addField(const QString & name,const QString & val,QString & tt)592 static void addField(const QString &name, const QString &val, QString &tt)
593 {
594     if (!val.isEmpty()) {
595         tt+=QString("<tr><td align=\"right\"><b>%1:&nbsp;&nbsp;</b></td><td>%2</td></tr>").arg(name).arg(val);
596     }
597 }
598 #endif
599 
toolTip() const600 QString Song::toolTip() const
601 {
602     #ifdef CANTATA_NO_UI_FUNCTIONS
603     return QString();
604     #else
605     QString toolTip=QLatin1String("<table>");
606     addField(QObject::tr("Title"), title, toolTip);
607     addField(QObject::tr("Artist"), artist, toolTip);
608     if (albumartist!=artist) {
609         addField(QObject::tr("Album artist"), albumartist, toolTip);
610     }
611     addField(QObject::tr("Composer"), composer(), toolTip);
612     addField(QObject::tr("Performer"), performer(), toolTip);
613     addField(QObject::tr("Album"), albumName(), toolTip);
614     if (track>0) {
615         addField(QObject::tr("Track number"), QString::number(track), toolTip);
616     }
617     if (disc>0) {
618         addField(QObject::tr("Disc number"), QString::number(disc), toolTip);
619     }
620     addField(QObject::tr("Genre"), displayGenre(), toolTip);
621     if (year>0) {
622         addField(QObject::tr("Year"), QString::number(year), toolTip);
623     }
624     if (origYear>0) {
625         addField(QObject::tr("Original Year"), QString::number(origYear), toolTip);
626     }
627     if (time>0) {
628         addField(QObject::tr("Length"), Utils::formatTime(time, true), toolTip);
629     }
630     toolTip+=QLatin1String("</table>");
631 
632     if (isNonMPD()) {
633         return toolTip;
634     }
635     return toolTip+QLatin1String("<br/><br/><small><i>")+filePath()+QLatin1String("</i></small>");
636     #endif
637 }
638 
displayGenre() const639 QString Song::displayGenre() const
640 {
641     QString g=genres[0];
642     for (int i=1; i<constNumGenres&& !genres[i].isEmpty(); ++i) {
643         g+=QLatin1String(", ")+genres[i];
644     }
645     return g;
646 }
647 
compareGenres(const Song & o) const648 int Song::compareGenres(const Song &o) const
649 {
650     for (int i=0; i<constNumGenres; ++i) {
651         int compare=genres[i].compare(o.genres[i]);
652         if (0!=compare) {
653             return compare;
654         }
655     }
656     return 0;
657 }
658 
setExtraField(quint16 f,const QString & v)659 void Song::setExtraField(quint16 f, const QString &v)
660 {
661     if (v.isEmpty()) {
662         extra.remove(f);
663         extraFields&=~f;
664     } else {
665         extra[f]=v;
666         extraFields|=f;
667     }
668 }
669 
isVariousArtists(const QString & str)670 bool Song::isVariousArtists(const QString &str)
671 {
672     return QLatin1String("Various Artists")==str || variousArtistsStr==str;
673 }
674 
diffArtist() const675 bool Song::diffArtist() const
676 {
677     return /*isVariousArtists() || */(!albumartist.isEmpty() && !artist.isEmpty() && albumartist!=artist);
678 }
679 
fixVariousArtists()680 bool Song::fixVariousArtists()
681 {
682     if (isVariousArtists()) {
683         artist.replace(" - ", ", ");
684         title=artistSong();
685         artist=albumartist;
686         return true;
687     }
688     return false;
689 }
690 
revertVariousArtists()691 bool Song::revertVariousArtists()
692 {
693     if (artist==albumartist) { // Then real artist is embedded in track title...
694         int sepPos=title.indexOf(QLatin1String(" - "));
695         if (sepPos>0 && sepPos<title.length()-3) {
696             artist=title.left(sepPos);
697             title=title.mid(sepPos+3);
698             return true;
699         }
700     }
701 
702     return false;
703 }
704 
setAlbumArtist()705 bool Song::setAlbumArtist()
706 {
707     if (!artist.isEmpty() && albumartist.isEmpty()) {
708         albumartist=artist;
709         return true;
710     }
711     return false;
712 }
713 
capitalize(const QString & s)714 QString Song::capitalize(const QString &s)
715 {
716     if (s.isEmpty()) {
717         return s;
718     }
719 
720     QStringList words = s.split(' ', QString::SkipEmptyParts);
721     for (int i = 0; i < words.count(); i++) {
722         QString word = words[i]; //.toLower();
723         int j = 0;
724         while ( j < word.length() && ('('==word[j] || '['==word[j] || '{'==word[j]) ) {
725             j++;
726         }
727         if (j < word.length()) {
728             word[j] = word[j].toUpper();
729         }
730         words[i] = word;
731     }
732     return words.join(" ");
733 }
734 
capitalise()735 bool Song::capitalise()
736 {
737     QString origArtist=artist;
738     QString origAlbumArtist=albumartist;
739     QString origAlbum=album;
740     QString origTitle=title;
741 
742     artist=capitalize(artist);
743     albumartist=capitalize(albumartist);
744     album=capitalize(album);
745     title=capitalize(title);
746     QString c=composer();
747     if (!c.isEmpty()) {
748         setComposer(capitalize(c));
749     }
750     /* Performer is not currently in tag editor...
751     QString p=performer();
752     if (!p.isEmpty()) {
753         setPerformer(capitalize(p));
754     }
755     */
756 
757     return artist!=origArtist || albumartist!=origAlbumArtist || album!=origAlbum || title!=origTitle ||
758            (!c.isEmpty() && c!=composer()) /*|| (!p.isEmpty() && p!=performer())*/ ;
759 }
760 
albumKey() const761 QString Song::albumKey() const
762 {
763     #if !defined CANTATA_NO_UI_FUNCTIONS
764     if ((OnlineSvrTrack==type || Song::CantataStream) && OnlineService::showLogoAsCover(*this)) {
765         return onlineService();
766     }
767     #endif
768     return albumArtistOrComposer()+QLatin1Char(':')+albumId(); //+QLatin1Char(':')+QString::number(disc);
769 }
770 
basic(const QString & str,const QStringList & extraToStrip=QStringList ())771 static QString basic(const QString &str, const QStringList &extraToStrip=QStringList())
772 {
773     QStringList toStrip=QStringList() << QLatin1String("ft. ") << QLatin1String("feat. ") << QLatin1String("featuring ") << QLatin1String("f. ")
774                                       << extraToStrip;
775     QStringList prefixes=QStringList() << QLatin1String(" ") << QLatin1String(" (") << QLatin1String(" [");
776 
777     for (const QString &s: toStrip) {
778         for (const QString &p: prefixes) {
779             int strip = str.toLower().indexOf(p+s);
780             if (-1!=strip) {
781                 return str.mid(0, strip);
782             }
783         }
784     }
785     return str;
786 }
787 
basicArtist(bool orComposer) const788 QString Song::basicArtist(bool orComposer) const
789 {
790     if (orComposer && useComposer()) {
791         QString c=composer();
792         if (!c.isEmpty()) {
793             return c;
794         }
795     }
796 
797     if (!albumartist.isEmpty() && (artist.isEmpty() || albumartist==artist || (albumartist.length()<artist.length() && artist.startsWith(albumartist)))) {
798         return albumartist;
799     }
800 
801     return basic(artist);
802 }
803 
basicTitle() const804 QString Song::basicTitle() const
805 {
806     return basic(title, QStringList() << QLatin1String("prod. ") << QLatin1String("prod ") << QLatin1String("producer ") << QLatin1String("produced by "));
807 }
808 
filePath(const QString & base) const809 QString Song::filePath(const QString &base) const
810 {
811     if (isCantataStream()) {
812         QString p = localPath();
813         return p.isEmpty() ? QUrl(file).path() : p;
814     }
815     if (isLocalFile()) {
816         return file;
817     }
818     QString fileName=decodePath(file, isCdda());
819     if (!base.isEmpty() && !fileName.isEmpty() && !isNonMPD()) {
820         bool haveAbsPath=fileName.startsWith("/") || fileName.contains(":/");
821         if (!haveAbsPath) {
822             return QString(base+fileName);
823         }
824     }
825     return fileName;
826 }
827 
describe() const828 QString Song::describe() const
829 {
830     #if !defined CANTATA_NO_UI_FUNCTIONS
831     if (OnlineSvrTrack==type && PodcastService::constName==onlineService()) {
832         return title;
833     }
834     #endif
835 
836     QString albumText=album.isEmpty() ? name() : displayAlbum(album, Song::albumYear(*this));
837 
838     if (title.isEmpty()) {
839         return QLatin1String("<b>")+albumText+QLatin1String("</b>");
840     }
841     return artist.isEmpty()
842                     ? QObject::tr("<b>%1</b> on <b>%2</b>", "Song on Album").arg(title).arg(albumText)
843                     : QObject::tr("<b>%1</b> by <b>%2</b> on <b>%3</b>", "Song by Artist on Album").arg(title).arg(artist).arg(albumText);
844 }
845 
mainText() const846 QString Song::mainText() const
847 {
848     if (isEmpty()) {
849         return QString();
850     }
851 
852     QString n = name();
853     if (isStream() && !isCantataStream() && !isCdda() && !isDlnaStream()) {
854         return n.isEmpty() ? Song::unknown() : n;
855     } else if (title.isEmpty() && artist.isEmpty() && (!n.isEmpty() || !file.isEmpty())) {
856         return n.isEmpty() ? file : n;
857     } else {
858         return title+(origYear>0 && !Song::useOriginalYear() && origYear!=year ? QLatin1String(" (")+QString::number(origYear)+QLatin1Char(')') : QString());
859     }
860 }
861 
subText() const862 QString Song::subText() const
863 {
864     if (isEmpty()) {
865         return QString();
866     }
867 
868     if (isStream() && !isCantataStream() && !isCdda() && !isDlnaStream()) {
869         if (artist.isEmpty() && title.isEmpty() && !name().isEmpty()) {
870             return QObject::tr("(Stream)");
871         } else {
872             return artist.isEmpty() ? title : artistSong();
873         }
874     } else if (album.isEmpty() && artist.isEmpty() && (!useComposer() || composer().isEmpty())) {
875         return mainText().isEmpty() ? QString() : Song::unknown();
876     } else {
877         QString comp = useComposer() ? composer() : QString();
878         if (album.isEmpty()) {
879             return artist.isEmpty() ? comp : comp.isEmpty() ? artist : (comp + constSep + artist);
880         } else {
881             // Artist here is always artist (or 'composer - artist'), and not album artist
882             return (artist.isEmpty() ? comp : comp.isEmpty() ? artist : (comp + constSep + artist)) + constSep+displayAlbum(!comp.isEmpty());
883         }
884     }
885 }
886 
useComposer() const887 bool Song::useComposer() const
888 {
889     if (compGenres.isEmpty()) {
890         return false;
891     }
892 
893     for (int i=0; i<constNumGenres && !genres[i].isEmpty(); ++i) {
894         if (compGenres.contains(genres[i])) {
895             return true;
896         }
897     }
898     return false;
899 }
900 
populateSorts()901 void Song::populateSorts()
902 {
903     if (!hasArtistSort()) {
904         QString val=sortString(artist);
905         if (val!=artist) {
906             setArtistSort(val);
907         }
908     }
909     if (!albumartist.isEmpty() && !hasAlbumArtistSort()) {
910         QString val=sortString(albumartist);
911         if (val!=albumartist) {
912             setAlbumArtistSort(val);
913         }
914     }
915     if (!hasAlbumSort()) {
916         QString val=sortString(album);
917         if (val!=album) {
918             setAlbumSort(val);
919         }
920     }
921 }
922 
setFromSingleTracks()923 void Song::setFromSingleTracks()
924 {
925     albumartist=variousArtists();
926     album=singleTracks();
927     type=SingleTracks;
928     setAlbumArtistSort(QString());
929     setAlbumSort(QString());
930     setMbAlbumId(QString());
931 }
932 
933 //QString Song::basicDescription() const
934 //{
935 //    return isStandardStream()
936 //            ? name
937 //            : title.isEmpty()
938 //                ? QString()
939 //                : artist.isEmpty()
940 //                    ? title
941 //                    : trc("track – artist", "%1 – %2", title, artist);
942 //}
943 
operator <<(QDataStream & stream,const Song & song)944 QDataStream & operator<<(QDataStream &stream, const Song &song)
945 {
946     stream << song.id << song.file << song.album << song.artist << song.albumartist << song.title
947            << song.disc << song.priority << song.time << song.track << (quint16)song.year // << song.origYear
948            << (quint16)song.type << (bool)song.guessed << song.size << song.extra << song.extraFields;
949     for (int i=0; i<Song::constNumGenres; ++i) {
950         stream << song.genres[i];
951     }
952     return stream;
953 }
954 
operator >>(QDataStream & stream,Song & song)955 QDataStream & operator>>(QDataStream &stream, Song &song)
956 {
957     quint16 type;
958     quint16 year;
959     quint8 disc;
960     bool guessed;
961     stream >> song.id >> song.file >> song.album >> song.artist >> song.albumartist >> song.title
962            >> disc >> song.priority >> song.time >> song.track >> year // >> song.origYear
963            >> type >> guessed >> song.size >> song.extra >> song.extraFields;
964     song.type=(Song::Type)type;
965     song.year=year;
966     song.guessed=guessed;
967     song.disc=disc;
968     for (int i=0; i<Song::constNumGenres; ++i) {
969         stream >> song.genres[i];
970     }
971 
972     return stream;
973 }
974