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: </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