diff -Nru cantata-2.3.2/ChangeLog cantata-2.3.3/ChangeLog --- cantata-2.3.2/ChangeLog 2018-08-02 17:30:25.000000000 +0000 +++ cantata-2.3.3/ChangeLog 2018-12-09 19:44:39.000000000 +0000 @@ -1,3 +1,42 @@ +2.3.3 +----- + 1. For Opus files, use R128_TRACK_GAIN and R128_ALBUM_GAIN to store replaygain + values. + 2. Remove user-agent checking when serving local files, this is easily + fake-able and breaks playback to forked-daap (and mopidy?) + 3. Add '.opus' to list of recognised extensions for local files. + 4. Initialise network proxy factory when starting. + 5. If artist, album, and title are empty in replaygain dialog, then show + filename in title column. + 6. Opus does not use replaygain peak tags, so do not write. + 7. Use same 'album key' for all discs in an album, so that playqueue groups + them together, and shuffle by albums keeps them together. + 8. Remove confirmation dialog when saving replaygain tags. + 9. Fix saving 'Descending' order for smart playlists. +10. When getting 'basic' title of song, also remove any 'prod. XXX', etc. + values. +11. Allow .jpeg as extension from cover dialog. +12. Fix QMediaPlayer stuck with network streams - thanks to theirix +13. Always show volume control. +14. Fix 'Show Current Song Information' (i) toolbar button showing when + interface is collpased and resized. +15. When expand intrface, don't shrink width. Conversely, when collapsing + don't expand width. +16. In grouped style playqueue, only show album duration if there is more than + one track from the album. +17. Don't try to reduce brackets when showing album name and year. +18. Add option to sort smart playlists by title. +19. Change toolbar colours if palette changes. +20. Add another qt5ct palette work-around. +21. Don't stop library scan just because of failure in 1 directory. +22. Handle empty VolumeIdentifier in MTP devices. +23. Add more actions to search page results. +24. For MPD>=21, use its albumart protocol to fetch covers. +25. When copying tracks to a device, only update cache if configured to do so. +26. Fix MusicBrainz disc ID calculation. +27. When loading URLs via commandline use AppendAndPlay. +28. MPRIS seeks command specifies an offset from current position. + 2.3.2 ----- 1. Store actual song path for local files (mainly affects windows) diff -Nru cantata-2.3.2/CMakeLists.txt cantata-2.3.3/CMakeLists.txt --- cantata-2.3.2/CMakeLists.txt 2018-07-27 21:44:20.000000000 +0000 +++ cantata-2.3.3/CMakeLists.txt 2018-10-20 22:39:39.000000000 +0000 @@ -28,7 +28,7 @@ set(CPACK_SOURCE_GENERATOR "TBZ2") set(CPACK_PACKAGE_VERSION_MAJOR "2") set(CPACK_PACKAGE_VERSION_MINOR "3") -set(CPACK_PACKAGE_VERSION_PATCH "2") +set(CPACK_PACKAGE_VERSION_PATCH "3") set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_PACKAGE_VERSION_SPIN "") # Use ".$number" - e.g. ".1" set(CPACK_PACKAGE_CONTACT "Craig Drummond ") @@ -37,7 +37,7 @@ set(CANTATA_VERSION_WITH_SPIN "${CANTATA_VERSION_FULL}${CPACK_PACKAGE_VERSION_SPIN}") set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}-${CANTATA_VERSION_WITH_SPIN}") set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}-${CANTATA_VERSION_WITH_SPIN}") -set(CPACK_SOURCE_IGNORE_FILES "/.svn/;/.git/;.gitignore;.project;CMakeLists.txt.user;README.md;/screenshots/") +set(CPACK_SOURCE_IGNORE_FILES "/.svn/;/.git/;.gitignore;.github/;.project;CMakeLists.txt.user;README.md;/screenshots/") include(CPack) include(MacroLogFeature) diff -Nru cantata-2.3.2/dbus/mpris.cpp cantata-2.3.3/dbus/mpris.cpp --- cantata-2.3.2/dbus/mpris.cpp 2018-03-07 19:04:41.000000000 +0000 +++ cantata-2.3.3/dbus/mpris.cpp 2018-12-09 19:42:36.000000000 +0000 @@ -46,6 +46,7 @@ connect(this, SIGNAL(setRandom(bool)), MPDConnection::self(), SLOT(setRandom(bool))); connect(this, SIGNAL(setRepeat(bool)), MPDConnection::self(), SLOT(setRepeat(bool))); connect(this, SIGNAL(setSeekId(qint32, quint32)), MPDConnection::self(), SLOT(setSeekId(qint32, quint32))); + connect(this, SIGNAL(seek(qint32)), MPDConnection::self(), SLOT(seek(qint32))); connect(this, SIGNAL(setVolume(int)), MPDConnection::self(), SLOT(setVolume(int))); // connect(MPDConnection::self(), SIGNAL(currentSongUpdated(const Song &)), this, SLOT(updateCurrentSong(const Song &))); diff -Nru cantata-2.3.2/dbus/mpris.h cantata-2.3.3/dbus/mpris.h --- cantata-2.3.2/dbus/mpris.h 2018-03-07 19:04:41.000000000 +0000 +++ cantata-2.3.3/dbus/mpris.h 2018-12-09 19:44:08.000000000 +0000 @@ -76,7 +76,7 @@ void Stop() { StdActions::self()->stopPlaybackAction->trigger(); } void StopAfterCurrent() { StdActions::self()->stopAfterCurrentTrackAction->trigger(); } void Play(); - void Seek(qlonglong pos) { emit setSeekId(-1, pos/1000000); } + void Seek(qlonglong pos) { emit seek(pos/1000000); } void SetPosition(const QDBusObjectPath &trackId, qlonglong pos); void OpenUri(const QString &) { } QString PlaybackStatus() const; @@ -120,6 +120,7 @@ void setRandom(bool toggle); void setRepeat(bool toggle); void setSeekId(qint32 songId, quint32 time); + void seek(qint32 offset); void setVolume(int vol); void showMainWindow(); diff -Nru cantata-2.3.2/debian/changelog cantata-2.3.3/debian/changelog --- cantata-2.3.2/debian/changelog 2017-09-30 05:41:23.000000000 +0000 +++ cantata-2.3.3/debian/changelog 2017-09-30 05:41:23.000000000 +0000 @@ -1,5 +1,5 @@ -cantata (2.3.2-2build1~ubuntu18.04) bionic; urgency=low +cantata (2.3.3-0build1~ubuntu18.04) bionic; urgency=low - * add libqt5multimedia5-plugins as dependency (Thanks to Pablo). + * new upstream release. -- pandajim (key for lives deb) Sat, 30 Sep 2017 13:41:23 +0800 diff -Nru cantata-2.3.2/devices/actiondialog.cpp cantata-2.3.3/devices/actiondialog.cpp --- cantata-2.3.2/devices/actiondialog.cpp 2018-05-24 18:11:34.000000000 +0000 +++ cantata-2.3.3/devices/actiondialog.cpp 2018-11-25 13:03:35.000000000 +0000 @@ -847,7 +847,7 @@ emit update(); Device *dev=DevicesModel::self()->device(sourceUdi.isEmpty() ? destUdi : sourceUdi); - if (dev) { + if (dev && dev->options().useCache) { connect(dev, SIGNAL(cacheSaved()), this, SLOT(cacheSaved())); dev->saveCache(); progressLabel->setText(tr("Saving cache")); @@ -863,7 +863,7 @@ (Remove==mode && !sourceUdi.isEmpty()) ) { Device *dev=DevicesModel::self()->device(sourceUdi.isEmpty() ? destUdi : sourceUdi); - if (dev) { + if (dev && dev->options().useCache) { connect(dev, SIGNAL(cacheSaved()), this, SLOT(cacheSaved())); dev->saveCache(); progressLabel->setText(tr("Saving cache")); diff -Nru cantata-2.3.2/devices/cdparanoia.cpp cantata-2.3.3/devices/cdparanoia.cpp --- cantata-2.3.2/devices/cdparanoia.cpp 2018-01-01 10:09:03.000000000 +0000 +++ cantata-2.3.3/devices/cdparanoia.cpp 2018-08-10 09:02:42.000000000 +0000 @@ -30,12 +30,13 @@ static QSet lockedDevices; static QMutex mutex; -CdParanoia::CdParanoia(const QString &device, bool full, bool noSkip, bool playback) +CdParanoia::CdParanoia(const QString &device, bool full, bool noSkip, bool playback, int offset) : drive(0) , paranoia(0) , paranoiaMode(0) , neverSkip(noSkip) , maxRetries(20) + , seekOffst(offset) { QMutexLocker locker(&mutex); if (!lockedDevices.contains(device)) { @@ -103,9 +104,9 @@ int CdParanoia::seek(long sector, int mode) { #ifdef CDIOPARANOIA_FOUND - return paranoia ? cdio_paranoia_seek(paranoia, sector, mode) : -1; + return paranoia ? cdio_paranoia_seek(paranoia, sector+seekOffst, mode) : -1; #else - return paranoia ? paranoia_seek(paranoia, sector, mode) : -1; + return paranoia ? paranoia_seek(paranoia, sector+seekOffst, mode) : -1; #endif } diff -Nru cantata-2.3.2/devices/cdparanoia.h cantata-2.3.3/devices/cdparanoia.h --- cantata-2.3.2/devices/cdparanoia.h 2018-01-01 10:09:04.000000000 +0000 +++ cantata-2.3.3/devices/cdparanoia.h 2018-08-10 09:04:49.000000000 +0000 @@ -51,7 +51,7 @@ class CdParanoia { public: - CdParanoia(const QString &device, bool full, bool noSkip, bool playback=false); + explicit CdParanoia(const QString &device, bool full, bool noSkip, bool playback, int offset); ~CdParanoia(); inline operator bool() const { return !dev.isEmpty(); } @@ -90,6 +90,7 @@ int paranoiaMode; bool neverSkip; int maxRetries; + int seekOffst; }; #endif diff -Nru cantata-2.3.2/devices/extractjob.cpp cantata-2.3.3/devices/extractjob.cpp --- cantata-2.3.2/devices/extractjob.cpp 2018-03-07 19:04:41.000000000 +0000 +++ cantata-2.3.3/devices/extractjob.cpp 2018-10-20 22:39:39.000000000 +0000 @@ -87,7 +87,7 @@ emit result(Device::Cancelled); } else { QStringList encParams=encoder.params(value, encoder.transcoder ? "pipe:" : "-", destFile); - CdParanoia cdparanoia(srcFile, Settings::self()->paranoiaFull(), Settings::self()->paranoiaNeverSkip()); + CdParanoia cdparanoia(srcFile, Settings::self()->paranoiaFull(), Settings::self()->paranoiaNeverSkip(), false, Settings::self()->paranoiaOffset()); if (!cdparanoia) { emit result(Device::FailedToLockDevice); @@ -153,7 +153,7 @@ Tags::update(destFile, Song(), song, 3); if (!stopRequested && !coverFile.isEmpty()) { - copiedCover=Covers::copyImage(Utils::getDir(coverFile), Utils::getDir(destFile), Utils::getFile(coverFile), Covers::albumFileName(song)+coverFile.mid(coverFile.length()-4), 0); + copiedCover=Covers::copyImage(Utils::getDir(coverFile), Utils::getDir(destFile), Utils::getFile(coverFile), Covers::albumFileName(song)+Utils::getExtension(coverFile), 0); } emit result(Device::Ok); diff -Nru cantata-2.3.2/devices/mtpdevice.cpp cantata-2.3.3/devices/mtpdevice.cpp --- cantata-2.3.2/devices/mtpdevice.cpp 2018-05-24 17:03:17.000000000 +0000 +++ cantata-2.3.3/devices/mtpdevice.cpp 2018-10-23 20:02:09.000000000 +0000 @@ -580,6 +580,12 @@ LIBMTP_devicestorage_struct *s=device->storage; while (s) { QString volumeIdentifier=QString::fromUtf8(s->VolumeIdentifier); + if (volumeIdentifier.isEmpty()) { + volumeIdentifier=QString::fromUtf8(s->StorageDescription); + } + if (volumeIdentifier.isEmpty()) { + volumeIdentifier="ID:"+QString::number(s->id); + } QList::Iterator it=storage.begin(); QList::Iterator end=storage.end(); for ( ;it!=end; ++it) { diff -Nru cantata-2.3.2/devices/musicbrainz.cpp cantata-2.3.3/devices/musicbrainz.cpp --- cantata-2.3.2/devices/musicbrainz.cpp 2018-03-07 19:04:41.000000000 +0000 +++ cantata-2.3.3/devices/musicbrainz.cpp 2018-11-29 20:47:40.000000000 +0000 @@ -91,7 +91,7 @@ sha.addData(temp, strlen(temp)); for(int i = 0; i < 100; i++) { - long offset; + int offset; if (0==i) { offset = tracks[numTracks].offset; } else if (i <= numTracks) { @@ -100,7 +100,7 @@ offset = 0; } - sprintf(temp, "%08lX", offset); + sprintf(temp, "%08X", offset); sha.addData(temp, strlen(temp)); } @@ -192,7 +192,7 @@ } te.cdte_track = CDROM_LEADOUT; if (0==ioctl(fd, CDROMREADTOCENTRY, &te)) { - tracks.append((te.cdte_addr.lba+secondsToFrames(2))/secondsToFrames(1)); + tracks.append((te.cdte_addr.lba+secondsToFrames(2))); } } #endif @@ -217,6 +217,7 @@ s.albumartist=initial.artist; s.album=initial.name; s.id=s.track; + s.time=framesToSeconds((next.offset-trk.offset)-(next.isData ? constDataTrackAdjust : 0)); s.file=QString("%1.wav").arg(s.track); s.year=initial.year; diff -Nru cantata-2.3.2/gui/application.cpp cantata-2.3.3/gui/application.cpp --- cantata-2.3.2/gui/application.cpp 2018-06-01 16:26:03.000000000 +0000 +++ cantata-2.3.3/gui/application.cpp 2018-08-14 14:56:46.000000000 +0000 @@ -42,6 +42,7 @@ #include "widgets/toolbutton.h" #include "widgets/sizegrip.h" #include "http/httpserver.h" +#include "network/networkproxyfactory.h" #include "config.h" void Application::init() @@ -78,6 +79,7 @@ #ifdef ENABLE_TAGLIB TagHelperIface::self(); #endif + NetworkProxyFactory::self(); Scrobbler::self(); MpdLibraryModel::self(); PlaylistsModel::self(); diff -Nru cantata-2.3.2/gui/coverdialog.cpp cantata-2.3.3/gui/coverdialog.cpp --- cantata-2.3.2/gui/coverdialog.cpp 2018-05-24 17:55:11.000000000 +0000 +++ cantata-2.3.3/gui/coverdialog.cpp 2018-10-20 22:39:39.000000000 +0000 @@ -439,7 +439,7 @@ Add other images in the source folder? #716 if (!isArtist) { QString dirName=MPDConnection::self()->getDetails().dir+Utils::getDir(s.file); - QStringList files=QDir(dirName).entryList(QStringList() << QLatin1String("*.jpg") << QLatin1String("*.png"), QDir::Files|QDir::Readable); + QStringList files=QDir(dirName).entryList(QStringList() << QLatin1String("*.jpg") << QLatin1String("*.jpeg") << QLatin1String("*.png"), QDir::Files|QDir::Readable); qWarning() << files; for (const QString &f: files) { QString fileName=dirName+f; @@ -803,7 +803,7 @@ void CoverDialog::addLocalFile() { - QString fileName=QFileDialog::getOpenFileName(this, tr("Load Local Cover"), QDir::homePath(), tr("Images (*.png *.jpg)")); + QString fileName=QFileDialog::getOpenFileName(this, tr("Load Local Cover"), QDir::homePath(), tr("Images (*.png *.jpg *.jpeg)")); if (!fileName.isEmpty()) { if (currentLocalCovers.contains(fileName)) { @@ -1188,10 +1188,14 @@ bool CoverDialog::saveCover(const QString &src, const QImage &img) { QString filePath=song.filePath(); + QString ext = Utils::getExtension(src); + if (0==ext.compare(".jpeg", Qt::CaseInsensitive)) { + ext = ".jpg"; + } if (song.isCdda()) { QString dir = Utils::cacheDir(Covers::constCddaCoverDir, true); if (!dir.isEmpty()) { - QString destName=dir+filePath.mid(Song::constCddaProtocol.length())+src.mid(src.length()-4); + QString destName=dir+filePath.mid(Song::constCddaProtocol.length())+ext; if (QFile::exists(destName)) { QFile::remove(destName); } @@ -1233,16 +1237,16 @@ if (saveInMpd && !mpdDir.isEmpty() && dirName.startsWith(mpdDir) && 2==dirName.mid(mpdDir.length()).split('/', QString::SkipEmptyParts).count()) { QDir d(dirName); d.cdUp(); - destName=d.absolutePath()+'/'+Covers::constArtistImage+src.mid(src.length()-4); + destName=d.absolutePath()+'/'+Covers::constArtistImage+ext; } else { - destName=Utils::cacheDir(Covers::constCoverDir, true)+Covers::encodeName(song.albumArtist())+src.mid(src.length()-4); + destName=Utils::cacheDir(Covers::constCoverDir, true)+Covers::encodeName(song.albumArtist())+ext; } } else { if (saveInMpd) { - destName=dirName+Covers::albumFileName(song)+src.mid(src.length()-4); + destName=dirName+Covers::albumFileName(song)+ext; } else { // Save to cache dir... QString dir(Utils::cacheDir(Covers::constCoverDir+Covers::encodeName(song.albumArtist()), true)); - destName=dir+Covers::encodeName(song.album)+src.mid(src.length()-4); + destName=dir+Covers::encodeName(song.album)+ext; } } @@ -1283,7 +1287,7 @@ for (const QUrl &url: urls) { if (url.scheme().isEmpty() || "file"==url.scheme()) { QString path=url.path(); - if (!currentLocalCovers.contains(path) && (path.endsWith(".jpg", Qt::CaseInsensitive) || path.endsWith(".png", Qt::CaseInsensitive))) { + if (!currentLocalCovers.contains(path) && (path.endsWith(".jpg", Qt::CaseInsensitive) || path.endsWith(".jpeg", Qt::CaseInsensitive) || path.endsWith(".png", Qt::CaseInsensitive))) { QImage img(path); if (!img.isNull()) { currentLocalCovers.insert(path); diff -Nru cantata-2.3.2/gui/covers.cpp cantata-2.3.3/gui/covers.cpp --- cantata-2.3.2/gui/covers.cpp 2018-06-08 15:55:10.000000000 +0000 +++ cantata-2.3.3/gui/covers.cpp 2018-11-12 19:39:28.000000000 +0000 @@ -594,6 +594,8 @@ thread=new Thread(metaObject()->className()); moveToThread(thread); thread->start(); + connect(this, SIGNAL(mpdCover(Song)), MPDConnection::self(), SLOT(getCover(Song))); + connect(MPDConnection::self(), SIGNAL(albumArt(Song,QByteArray)), this, SLOT(mpdAlbumArt(Song,QByteArray))); } void CoverDownloader::stop() @@ -636,7 +638,9 @@ Job job(song, dirName); - if (!MPDConnection::self()->getDetails().dir.isEmpty() && MPDConnection::self()->getDetails().dir.startsWith(QLatin1String("http://"))) { + if (!song.isArtistImageRequest() && !song.isComposerImageRequest() && MPDConnection::self()->supportsCoverDownload()) { + downloadViaMpd(job); + } else if (!MPDConnection::self()->getDetails().dir.isEmpty() && MPDConnection::self()->getDetails().dir.startsWith(QLatin1String("http://"))) { downloadViaHttp(job, JobHttpJpg); } else if (fetchCovers || job.song.isArtistImageRequest()) { downloadViaRemote(job); @@ -645,6 +649,13 @@ } } +void CoverDownloader::downloadViaMpd(Job &job) +{ + job.type=JobMpd; + emit mpdCover(job.song); + mpdJobs.insert(job.song.file, job); +} + bool CoverDownloader::downloadViaHttp(Job &job, JobType type) { QUrl u; @@ -710,6 +721,40 @@ DBUG << url.toString(); } +void CoverDownloader::mpdAlbumArt(const Song &song, const QByteArray &data) +{ + QHash::Iterator it = mpdJobs.find(song.file); + if (it!=mpdJobs.end()) { + Covers::Image img; + img.img= data.isEmpty() ? QImage() : QImage::fromData(data, Covers::imageFormat(data)); + Job job=it.value(); + + if (!img.img.isNull() && img.img.size().width()<32) { + img.img = QImage(); + } + + mpdJobs.remove(it.key()); + if (img.img.isNull()) { + downloadViaHttp(job, JobHttpJpg); + } else { + if (!img.img.isNull()) { + if (img.img.size().width()>Covers::constMaxSize.width() || img.img.size().height()>Covers::constMaxSize.height()) { + img.img=img.img.scaled(Covers::constMaxSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + img.fileName=saveImg(job, img.img, data); + if (!img.fileName.isEmpty()) { + clearScaledCache(job.song); + } + } + + DBUG << "got cover image" << img.fileName; + emit cover(job.song, img.img, img.fileName); + } + } else { + DBUG << "missing job for " << song.file; + } +} + void CoverDownloader::remoteCallFinished() { NetworkJob *reply=qobject_cast(sender()); @@ -1269,7 +1314,7 @@ QPixmap * Covers::get(const Song &song, int size, bool urgent) { - VERBOSE_DBUG_CLASS("Covers") << song.albumArtist() << song.album << song.mbAlbumId() << song.composer() << song.isArtistImageRequest() << song.isComposerImageRequest() << size << urgent; + VERBOSE_DBUG_CLASS("Covers") << song.albumArtist() << song.album << song.mbAlbumId() << song.composer() << song.isArtistImageRequest() << song.isComposerImageRequest() << size << urgent << song.type << song.isStandardStream() << isOnlineServiceImage(song); QString key; QPixmap *pix=nullptr; if (0==size) { @@ -1299,6 +1344,7 @@ } } if (pix) { + VERBOSE_DBUG_CLASS("Covers") << "Got standard image"; if (size!=origSize) { pix->setDevicePixelRatio(devicePixelRatio); VERBOSE_DBUG << "Set pixel ratio of cover" << devicePixelRatio; @@ -1597,9 +1643,9 @@ QStringList coverFileNames; if (song.isArtistImageRequest()) { QString basicArtist=song.basicArtist(); - coverFileNames=QStringList() << basicArtist+".jpg" << basicArtist+".png" << constArtistImage+".jpg" << constArtistImage+".png"; + coverFileNames=QStringList() << basicArtist+".jpg" << basicArtist+".png" << basicArtist+".jpeg" << constArtistImage+".jpg" << constArtistImage+".png" << constArtistImage+".jpeg"; } else if (song.isComposerImageRequest()) { - coverFileNames=QStringList() << constComposerImage+".jpg" << constComposerImage+".png"; + coverFileNames=QStringList() << constComposerImage+".jpg" << constComposerImage+".png" << constComposerImage+".jpeg"; } else { QString mpdCover=albumFileName(song); if (!mpdCover.isEmpty()) { @@ -1875,7 +1921,7 @@ QStringList entries=d.entryList(QDir::Files|QDir::NoSymLinks|QDir::NoDotAndDotDot); for (const QString &f: entries) { - if (f.endsWith(".jpg") || f.endsWith(".png")) { + if (f.endsWith(".jpg") || f.endsWith(".jpeg") || f.endsWith(".png")) { QFile::remove(dir+f); } } diff -Nru cantata-2.3.2/gui/covers.h cantata-2.3.3/gui/covers.h --- cantata-2.3.2/gui/covers.h 2018-03-14 18:58:20.000000000 +0000 +++ cantata-2.3.3/gui/covers.h 2018-11-12 19:39:28.000000000 +0000 @@ -48,6 +48,7 @@ public: enum JobType { + JobMpd, JobHttpJpg, JobHttpPng, JobRemote @@ -76,12 +77,15 @@ void cover(const Song &song, const QImage &img, const QString &file); void artistImage(const Song &song, const QImage &img, const QString &file); void composerImage(const Song &song, const QImage &img, const QString &file); + void mpdCover(const Song &song); private: + void downloadViaMpd(Job &job); bool downloadViaHttp(Job &job, JobType type); void downloadViaRemote(Job &job); private Q_SLOTS: + void mpdAlbumArt(const Song &song, const QByteArray &data); void remoteCallFinished(); void jobFinished(); void onlineJobFinished(); @@ -94,6 +98,7 @@ private: QHash jobs; + QHash mpdJobs; private: Thread *thread; diff -Nru cantata-2.3.2/gui/mainwindow.cpp cantata-2.3.3/gui/mainwindow.cpp --- cantata-2.3.2/gui/mainwindow.cpp 2018-07-09 15:35:13.000000000 +0000 +++ cantata-2.3.3/gui/mainwindow.cpp 2018-10-28 16:21:47.000000000 +0000 @@ -142,6 +142,7 @@ , lastSongId(-1) , autoScrollPlayQueue(true) , singlePane(false) + , shown(false) , currentPage(nullptr) #ifdef QT_QTDBUS_FOUND , mpris(nullptr) @@ -759,6 +760,7 @@ connect(devicesPage, SIGNAL(deleteSongs(const QString &, const QList &)), SLOT(deleteSongs(const QString &, const QList &))); connect(libraryPage, SIGNAL(deleteSongs(const QString &, const QList &)), SLOT(deleteSongs(const QString &, const QList &))); connect(folderPage->mpd(), SIGNAL(deleteSongs(const QString &, const QList &)), SLOT(deleteSongs(const QString &, const QList &))); + connect(searchPage, SIGNAL(deleteSongs(const QString &, const QList &)), SLOT(deleteSongs(const QString &, const QList &))); #endif for (QAction *act: StdActions::self()->setPriorityAction->menu()->actions()) { connect(act, SIGNAL(triggered()), this, SLOT(addWithPriority())); @@ -852,6 +854,7 @@ connect(locateArtistAction, SIGNAL(triggered()), this, SLOT(locateTrack())); connect(this, SIGNAL(playNext(QList,quint32,quint32)), MPDConnection::self(), SLOT(move(QList,quint32,quint32))); connect(playNextAction, SIGNAL(triggered()), this, SLOT(moveSelectionAfterCurrentSong())); + connect(qApp, SIGNAL(paletteChanged(const QPalette &)), this, SLOT(paletteChanged())); connect(StdActions::self()->searchAction, SIGNAL(triggered()), SLOT(showSearch())); connect(searchPlayQueueAction, SIGNAL(triggered()), this, SLOT(showPlayQueueSearch())); @@ -1108,6 +1111,11 @@ #endif QMainWindow::showEvent(event); controlView(); + if (!shown) { + shown=true; + // Work-around for qt5ct palette issues... + QTimer::singleShot(0, this, SLOT(paletteChanged())); + } } void MainWindow::closeEvent(QCloseEvent *event) @@ -1487,6 +1495,27 @@ #endif } +void MainWindow::paletteChanged() +{ + QColor before = nowPlaying->textColor(); + nowPlaying->initColors(); + if (before == nowPlaying->textColor()) { + return; + } + + volumeSlider->setColor(nowPlaying->textColor()); + + Icons::self()->initToolbarIcons(nowPlaying->textColor()); + StdActions::self()->prevTrackAction->setIcon(Icons::self()->toolbarPrevIcon); + StdActions::self()->nextTrackAction->setIcon(Icons::self()->toolbarNextIcon); + StdActions::self()->playPauseTrackAction->setIcon(MPDState_Playing==MPDStatus::self()->state() ? Icons::self()->toolbarPauseIcon : Icons::self()->toolbarPlayIcon); + StdActions::self()->stopPlaybackAction->setIcon(Icons::self()->toolbarStopIcon); + StdActions::self()->stopAfterCurrentTrackAction->setIcon(Icons::self()->toolbarStopIcon); + StdActions::self()->stopAfterTrackAction->setIcon(Icons::self()->toolbarStopIcon); + songInfoAction->setIcon(Icons::self()->infoIcon); + menuButton->setIcon(Icons::self()->toolbarMenuIcon); +} + void MainWindow::readSettings() { #ifdef QT_QTDBUS_FOUND @@ -2649,7 +2678,7 @@ stopTrackButton->setVisible(stopEnabled); } - if (responsiveSidebar || forceUpdate) { + if (expandInterfaceAction->isChecked() && (responsiveSidebar || forceUpdate)) { if (!responsiveSidebar || width()>Utils::scaleForDpi(450)) { if (forceUpdate || singlePane) { int index=tabWidget->currentIndex(); @@ -2734,17 +2763,14 @@ adjustSize(); if (showing) { - bool adjustWidth=size().width()!=expandedSize.width(); - bool adjustHeight=size().height()!=expandedSize.height(); - if (adjustWidth || adjustHeight) { - resize(adjustWidth ? expandedSize.width() : size().width(), adjustHeight ? expandedSize.height() : size().height()); - } + resize(qMax(prevWidth, expandedSize.width()), expandedSize.height()); if (lastMax) { showMaximized(); } + controlView(); } else { // Width also sometimes expands, so make sure this is no larger than it was before... - collapsedSize=QSize(collapsedSize.isValid() ? collapsedSize.width() : (size().width()>prevWidth ? prevWidth : size().width()), calcCollapsedSize()); + collapsedSize=QSize(collapsedSize.isValid() ? qMin(collapsedSize.width(), prevWidth) : prevWidth, calcCollapsedSize()); resize(collapsedSize); setFixedHeight(size().height()); } diff -Nru cantata-2.3.2/gui/mainwindow.h cantata-2.3.3/gui/mainwindow.h --- cantata-2.3.2/gui/mainwindow.h 2018-06-23 08:21:54.000000000 +0000 +++ cantata-2.3.3/gui/mainwindow.h 2018-12-03 22:49:52.000000000 +0000 @@ -154,7 +154,7 @@ void mpdConnectionName(const QString &name); void hideWindow(); void restoreWindow(); - void load(const QStringList &urls) { PlayQueueModel::self()->load(urls); } + void load(const QStringList &urls) { PlayQueueModel::self()->load(urls, MPDConnection::AppendAndPlay); } void showAboutDialog(); void mpdConnectionStateChanged(bool connected); void playQueueItemsSelected(bool s); @@ -268,6 +268,7 @@ void toggleContext(); void initMpris(); void toggleMenubar(); + void paletteChanged(); private: int prevPage; @@ -276,6 +277,7 @@ PlayQueueProxyModel playQueueProxyModel; bool autoScrollPlayQueue; bool singlePane; + bool shown; Action *prefAction; Action *refreshDbAction; Action *doDbRefreshAction; diff -Nru cantata-2.3.2/gui/searchpage.cpp cantata-2.3.3/gui/searchpage.cpp --- cantata-2.3.2/gui/searchpage.cpp 2018-03-07 19:04:41.000000000 +0000 +++ cantata-2.3.3/gui/searchpage.cpp 2018-10-28 16:22:07.000000000 +0000 @@ -28,6 +28,7 @@ #include "customactions.h" #include "support/utils.h" #include "support/icon.h" +#include "support/messagebox.h" #include "widgets/tableview.h" #include "widgets/menubutton.h" #include "widgets/icons.h" @@ -82,6 +83,15 @@ #ifdef ENABLE_DEVICES_SUPPORT view->addAction(StdActions::self()->copyToDeviceAction); #endif + view->addAction(StdActions::self()->organiseFilesAction); + view->addAction(StdActions::self()->editTagsAction); + #ifdef ENABLE_REPLAYGAIN_SUPPORT + view->addAction(StdActions::self()->replaygainAction); + #endif + #ifdef ENABLE_DEVICES_SUPPORT + view->addSeparator(); + view->addAction(StdActions::self()->deleteSongsAction); + #endif #endif // TAGLIB_FOUND view->addAction(locateAction); view->setInfoText(tr("Use the text field above to search for music in your library via MPD's search mechanism. " @@ -156,6 +166,19 @@ view->clearSelection(); } } + +void SearchPage::deleteSongs() +{ + QList songs=selectedSongs(); + + if (!songs.isEmpty()) { + if (MessageBox::Yes==MessageBox::warningYesNo(this, tr("Are you sure you wish to delete the selected songs?\n\nThis cannot be undone."), + tr("Delete Songs"), StdGuiItem::del(), StdGuiItem::cancel())) { + emit deleteSongs(QString(), songs); + } + view->clearSelection(); + } +} #endif void SearchPage::doSearch() @@ -186,6 +209,19 @@ StdActions::self()->enableAddToPlayQueue(enable); CustomActions::self()->setEnabled(enable); locateAction->setEnabled(enable); + + StdActions::self()->addToStoredPlaylistAction->setEnabled(enable); + #ifdef TAGLIB_FOUND + StdActions::self()->organiseFilesAction->setEnabled(enable && MPDConnection::self()->getDetails().dirReadable); + StdActions::self()->editTagsAction->setEnabled(StdActions::self()->organiseFilesAction->isEnabled()); + #ifdef ENABLE_REPLAYGAIN_SUPPORT + StdActions::self()->replaygainAction->setEnabled(StdActions::self()->organiseFilesAction->isEnabled()); + #endif + #ifdef ENABLE_DEVICES_SUPPORT + StdActions::self()->deleteSongsAction->setEnabled(StdActions::self()->organiseFilesAction->isEnabled()); + StdActions::self()->copyToDeviceAction->setEnabled(StdActions::self()->organiseFilesAction->isEnabled()); + #endif + #endif // TAGLIB_FOUND } void SearchPage::setSearchCategory(const QString &cat) diff -Nru cantata-2.3.2/gui/searchpage.h cantata-2.3.3/gui/searchpage.h --- cantata-2.3.2/gui/searchpage.h 2018-01-09 19:22:33.000000000 +0000 +++ cantata-2.3.3/gui/searchpage.h 2018-10-28 16:21:03.000000000 +0000 @@ -47,11 +47,13 @@ QList selectedSongs(bool allowPlaylists=false) const override; #ifdef ENABLE_DEVICES_SUPPORT void addSelectionToDevice(const QString &udi) override; + void deleteSongs() override; #endif void setSearchCategory(const QString &cat); Q_SIGNALS: void addToDevice(const QString &from, const QString &to, const QList &songs); + void deleteSongs(const QString &from, const QList &songs); void locate(const QList &songs); public Q_SLOTS: diff -Nru cantata-2.3.2/gui/settings.cpp cantata-2.3.3/gui/settings.cpp --- cantata-2.3.2/gui/settings.cpp 2018-03-24 15:35:26.000000000 +0000 +++ cantata-2.3.3/gui/settings.cpp 2018-08-10 09:01:42.000000000 +0000 @@ -537,6 +537,11 @@ { return cfg.get("paranoiaNeverSkip", true); } + +int Settings::paranoiaOffset() +{ + return cfg.get("paranoiaOffset", 0); +} #endif #if defined CDDB_FOUND && defined MUSICBRAINZ5_FOUND @@ -1012,6 +1017,11 @@ { cfg.set("paranoiaNeverSkip", v); } + +void Settings::saveParanoiaOffset(int v) +{ + cfg.set("paranoiaOffset", v); +} #endif #if defined CDDB_FOUND && defined MUSICBRAINZ5_FOUND diff -Nru cantata-2.3.2/gui/settings.h cantata-2.3.3/gui/settings.h --- cantata-2.3.2/gui/settings.h 2018-03-24 15:34:49.000000000 +0000 +++ cantata-2.3.3/gui/settings.h 2018-08-10 09:01:31.000000000 +0000 @@ -108,6 +108,7 @@ bool cdAuto(); bool paranoiaFull(); bool paranoiaNeverSkip(); + int paranoiaOffset(); #endif #if defined CDDB_FOUND && defined MUSICBRAINZ5_FOUND bool useCddb(); @@ -211,6 +212,7 @@ void saveCdAuto(bool v); void saveParanoiaFull(bool v); void saveParanoiaNeverSkip(bool v); + void saveParanoiaOffset(int v); #endif #if defined CDDB_FOUND && defined MUSICBRAINZ5_FOUND void saveUseCddb(bool v); diff -Nru cantata-2.3.2/http/httpsocket.cpp cantata-2.3.3/http/httpsocket.cpp --- cantata-2.3.2/http/httpsocket.cpp 2018-04-30 21:18:21.000000000 +0000 +++ cantata-2.3.3/http/httpsocket.cpp 2018-08-10 08:58:24.000000000 +0000 @@ -154,16 +154,6 @@ return rv; } -static bool isFromMpd(const QStringList ¶ms) -{ - for (const QString &str: params) { - if (str.startsWith("User-Agent:") && str.contains("Music Player Daemon")) { - return true; - } - } - return false; -} - static void getRange(const QStringList ¶ms, qint32 &from, qint32 &to) { for (const QString &str: params) { @@ -279,13 +269,6 @@ QStringList params = QString(socket->readAll()).split(QRegExp("[\r\n][\r\n]*")); DBUG << "params" << params << "tokens" << tokens; - if (!isFromMpd(params)) { - sendErrorResponse(socket, 400); - socket->close(); - DBUG << "Not from MPD"; - return; - } - QUrl url(QUrl::fromEncoded(tokens[1])); QUrlQuery q(url); bool ok=false; @@ -309,7 +292,7 @@ QStringList parts=song.file.split("/", QString::SkipEmptyParts); if (parts.length()>=3) { QString dev=QLatin1Char('/')+parts.at(1)+QLatin1Char('/')+parts.at(2); - CdParanoia cdparanoia(dev, false, false, true); + CdParanoia cdparanoia(dev, false, false, true, Settings::self()->paranoiaOffset()); if (cdparanoia) { int firstSector = cdparanoia.firstSectorOfTrack(song.id); diff -Nru cantata-2.3.2/models/playqueuemodel.cpp cantata-2.3.3/models/playqueuemodel.cpp --- cantata-2.3.2/models/playqueuemodel.cpp 2018-06-16 10:57:27.000000000 +0000 +++ cantata-2.3.3/models/playqueuemodel.cpp 2018-12-03 22:50:33.000000000 +0000 @@ -89,7 +89,7 @@ << QLatin1String("m4b") << QLatin1String("mp4") << QLatin1String("m4p") << QLatin1String("wav") << QLatin1String("wv") << QLatin1String("wvp") << QLatin1String("aiff") << QLatin1String("aif") << QLatin1String("aifc") << QLatin1String("ape") << QLatin1String("spx") << QLatin1String("tta") << QLatin1String("mpc") << QLatin1String("mpp") << QLatin1String("mp+") - << QLatin1String("dff") << QLatin1String("dsf") + << QLatin1String("dff") << QLatin1String("dsf") << QLatin1String("opus") // And playlists... << QLatin1String("m3u") << QLatin1String("m3u8") << constPlsPlaylist << constXspfPlaylist; @@ -927,7 +927,7 @@ if (MPDConnection::ReplaceAndplay==action) { emit filesAdded(filenames, 0, 0, MPDConnection::ReplaceAndplay, priority, decreasePriority); } else if (songs.isEmpty()) { - emit filesAdded(filenames, 0, 0, MPDConnection::Append, priority, decreasePriority); + emit filesAdded(filenames, 0, 0, action, priority, decreasePriority); } else if (row < 0) { emit filesAdded(filenames, songs.size(), songs.size(), action, priority, decreasePriority); } else { diff -Nru cantata-2.3.2/mpd-interface/httpstream.cpp cantata-2.3.3/mpd-interface/httpstream.cpp --- cantata-2.3.2/mpd-interface/httpstream.cpp 2018-07-19 16:45:37.000000000 +0000 +++ cantata-2.3.3/mpd-interface/httpstream.cpp 2018-10-23 20:02:34.000000000 +0000 @@ -32,15 +32,15 @@ #endif #include -static const int constPlayerCheckPeriod=250; -static const int constMaxPlayStateChecks=2000/constPlayerCheckPeriod; +static const int constPlayerCheckPeriod = 250; +static const int constMaxPlayStateChecks= 2000 / constPlayerCheckPeriod; #include -static bool debugEnabled=false; +static bool debugEnabled = false; #define DBUG if (debugEnabled) qWarning() << metaObject()->className() << __FUNCTION__ void HttpStream::enableDebug() { - debugEnabled=true; + debugEnabled = true; } GLOBAL_STATIC(HttpStream, instance) @@ -48,8 +48,8 @@ HttpStream::HttpStream(QObject *p) : QObject(p) , enabled(false) - , state(MPDState_Inactive) , muted(false) + , state(MPDState_Inactive) , playStateChecks(0) , currentVolume(50) , unmuteVol(50) @@ -66,11 +66,11 @@ void HttpStream::setEnabled(bool e) { - if (e==enabled) { + if (e == enabled) { return; } - enabled=e; + enabled = e; if (enabled) { connect(MPDConnection::self(), SIGNAL(streamUrl(QString)), this, SLOT(streamUrl(QString))); connect(MPDStatus::self(), SIGNAL(updated()), this, SLOT(updateStatus())); @@ -94,7 +94,7 @@ { DBUG << vol; if (player) { - currentVolume=vol; + currentVolume = vol; #ifdef LIBVLC_FOUND libvlc_audio_set_volume(player, vol); #else @@ -106,17 +106,21 @@ int HttpStream::volume() { - int vol=currentVolume; + if (!enabled) { + return -1; + } + + int vol = currentVolume; if (player && !isMuted()) { #ifdef LIBVLC_FOUND - vol=libvlc_audio_get_volume(player); + vol = libvlc_audio_get_volume(player); #else - vol=player->volume(); + vol = player->volume(); #endif - if (vol<0) { - vol=currentVolume; + if (vol < 0) { + vol = currentVolume; } else { - currentVolume=vol; + currentVolume = vol; } } DBUG << vol; @@ -127,7 +131,7 @@ { DBUG << isMuted(); if (player) { - muted=!muted; + muted =! muted; #ifdef LIBVLC_FOUND libvlc_audio_set_mute(player, muted); #else @@ -139,21 +143,22 @@ void HttpStream::streamUrl(const QString &url) { - MPDStatus * const status = MPDStatus::self(); DBUG << url; #ifdef LIBVLC_FOUND if (player) { libvlc_media_player_stop(player); libvlc_media_player_release(player); libvlc_release(instance); - player=0; + player = 0; } #else - static const char *constUrlProperty="url"; - if (player && player->property(constUrlProperty).toString()!=url) { - player->stop(); - player->deleteLater(); - player=nullptr; + if (player) { + QMediaContent media = player->media(); + if (media != nullptr && media.canonicalUrl() != url) { + player->stop(); + player->deleteLater(); + player = nullptr; + } } #endif QUrl qUrl(url); @@ -165,22 +170,36 @@ player = libvlc_media_player_new_from_media(media); libvlc_media_release(media); #else - player=new QMediaPlayer(this); + player = new QMediaPlayer(this); player->setMedia(qUrl); - player->setProperty(constUrlProperty, url); + connect(player, &QMediaPlayer::bufferStatusChanged, this, &HttpStream::bufferingProgress); #endif - muted=false; + muted = false; setVolume(Configuration(metaObject()->className()).get("volume", currentVolume)); } if (player) { - state=0xFFFF; // Force an update... + state = 0xFFFF; // Force an update... updateStatus(); } else { - state=MPDState_Inactive; + state = MPDState_Inactive; } emit update(); } +#ifndef LIBVLC_FOUND +void HttpStream::bufferingProgress(int progress) +{ + MPDStatus * const status = MPDStatus::self(); + if (status->state() == MPDState_Playing) { + if (progress == 100) { + player->play(); + } else { + player->pause(); + } + } +} +#endif + void HttpStream::updateStatus() { if (!player) { @@ -189,71 +208,72 @@ MPDStatus * const status = MPDStatus::self(); DBUG << status->state() << state; - if (status->state()==state) { + + // evaluates to true when it is needed to start or restart media player + bool playerNeedsToStart = status->state() == MPDState_Playing; + #ifdef LIBVLC_FOUND + playerNeedsToStart = playerNeedsToStart && libvlc_media_player_get_state(player) != libvlc_Playing; + #else + playerNeedsToStart = playerNeedsToStart && player->state() == QMediaPlayer::StoppedState; + #endif + + if (status->state() == state && !playerNeedsToStart) { return; } - state=status->state(); + state = status->state(); switch (status->state()) { case MPDState_Playing: // Only start playback if not aready playing + if (playerNeedsToStart) { #ifdef LIBVLC_FOUND - if (libvlc_Playing!=libvlc_media_player_get_state(player)) { libvlc_media_player_play(player); startTimer(); - } #else - if (QMediaPlayer::PlayingState!=player->state()) { - player->play(); - startTimer(); - } + QUrl url = player->media().canonicalUrl(); + player->setMedia(url); #endif + } break; case MPDState_Paused: case MPDState_Inactive: case MPDState_Stopped: #ifdef LIBVLC_FOUND libvlc_media_player_stop(player); + stopTimer(); #else player->stop(); #endif - stopTimer(); break; default: + #ifdef LIBVLC_FOUND stopTimer(); + #endif break; } } void HttpStream::checkPlayer() { - if (0==--playStateChecks) { + #ifdef LIBVLC_FOUND + if (0 == --playStateChecks) { stopTimer(); DBUG << "Max checks reached"; } - #ifdef LIBVLC_FOUND - if (libvlc_Playing==libvlc_media_player_get_state(player)) { + if (libvlc_media_player_get_state(player) == libvlc_Playing) { DBUG << "Playing"; stopTimer(); } else { DBUG << "Try again"; libvlc_media_player_play(player); } - #else - if (QMediaPlayer::PlayingState==player->state()) { - DBUG << "Playing"; - stopTimer(); - } else { - DBUG << "Try again"; - player->play(); - } #endif } void HttpStream::startTimer() { if (!playStateCheckTimer) { - playStateCheckTimer=new QTimer(this); + playStateCheckTimer = new QTimer(this); playStateCheckTimer->setSingleShot(false); playStateCheckTimer->setInterval(constPlayerCheckPeriod); connect(playStateCheckTimer, SIGNAL(timeout()), SLOT(checkPlayer())); @@ -269,7 +289,7 @@ DBUG; playStateCheckTimer->stop(); } - playStateChecks=0; + playStateChecks = 0; } #include "moc_httpstream.cpp" diff -Nru cantata-2.3.2/mpd-interface/httpstream.h cantata-2.3.3/mpd-interface/httpstream.h --- cantata-2.3.2/mpd-interface/httpstream.h 2018-02-09 20:10:41.000000000 +0000 +++ cantata-2.3.3/mpd-interface/httpstream.h 2018-10-23 20:02:34.000000000 +0000 @@ -37,7 +37,7 @@ class HttpStream : public QObject { Q_OBJECT - + public: static void enableDebug(); static HttpStream * self(); @@ -48,7 +48,7 @@ bool isMuted() const { return muted; } int volume(); int unmuteVolume() const { return unmuteVol; } - + Q_SIGNALS: void isEnabled(bool en); void update(); @@ -57,11 +57,14 @@ void setEnabled(bool e); void setVolume(int vol); void toggleMute(); - + private Q_SLOTS: void updateStatus(); void streamUrl(const QString &url); void checkPlayer(); +#ifndef LIBVLC_FOUND + void bufferingProgress(int progress); +#endif private: void startTimer(); @@ -82,8 +85,7 @@ libvlc_media_t *media; #else QMediaPlayer *player; - #endif + #endif }; #endif - diff -Nru cantata-2.3.2/mpd-interface/mpdconnection.cpp cantata-2.3.3/mpd-interface/mpdconnection.cpp --- cantata-2.3.2/mpd-interface/mpdconnection.cpp 2018-06-24 15:17:24.000000000 +0000 +++ cantata-2.3.3/mpd-interface/mpdconnection.cpp 2018-12-09 19:42:55.000000000 +0000 @@ -712,6 +712,18 @@ // setting commands are not supported. So, if we get this error then just ignore it. if (!isMpd() && (command.startsWith("crossfade ") || command.startsWith("replay_gain_mode "))) { emitError=false; + } else if (isMpd() && command.startsWith("albumart ")) { + // MPD will report a generic "file not found" error if it can't find album art; this can happen + // several times in a large playlist so hide this from the GUI (but report it using DBUG here). + emitError=false; + const auto start = command.indexOf(' '); + const auto end = command.lastIndexOf(' ') - start; + if (start > 0 && (end > 0 && (start + end) < command.length())) { + const QString filename = command.mid(start, end); + DBUG << "MPD reported no album art for" << filename; + } else { + DBUG << "MPD albumart command was malformed:" << command; + } } if (emitError) { if ((command.startsWith("add ") || command.startsWith("command_list_begin\nadd ")) && -1!=command.indexOf("\"file:///")) { @@ -1277,12 +1289,14 @@ sendCommand("play 0", emitErrors); } -void MPDConnection::seek() +void MPDConnection::seek(qint32 offset) { - QObject *s=sender(); - int offset=s ? s->property("offset").toInt() : 0; if (0==offset) { - return; + QObject *s=sender(); + offset=s ? s->property("offset").toInt() : 0; + if (0==offset) { + return; + } } toggleStopAfterCurrent(false); Response response=sendCommand("status"); @@ -1484,6 +1498,69 @@ } } +void MPDConnection::getCover(const Song &song) +{ + int dataToRead = -1; + int imageSize = 0; + QByteArray imageData; + bool firstRun = true; + QString path=Utils::getDir(song.file); + while (dataToRead != 0) { + Response response=sendCommand("albumart "+encodeName(path)+" "+QByteArray::number(firstRun ? 0 : (imageSize - dataToRead))); + if (!response.ok) { + DBUG << "albumart query failed"; + break; + } + + static const QByteArray constSize("size: "); + static const QByteArray constBinary("binary: "); + + auto sizeStart = strstr(response.data.constData(), constSize.constData()); + if (!sizeStart) { + DBUG << "Failed to get size start"; + break; + } + auto sizeEnd = strchr(sizeStart, '\n'); + if (!sizeEnd) { + DBUG << "Failed to get size end"; + break; + } + + auto chunkSizeStart = strstr(sizeEnd, constBinary.constData()); + if (!chunkSizeStart) { + DBUG << "Failed to get chunk size start"; + break; + } + auto chunkSizeEnd = strchr(chunkSizeStart, '\n'); + if (!chunkSizeEnd) { + DBUG << "Failed to chunk size end"; + break; + } + + if (firstRun) { + imageSize = QByteArray(sizeStart+constSize.length(), sizeEnd-(sizeStart+constSize.length())).toUInt(); + imageData.reserve(imageSize); + dataToRead = imageSize; + firstRun = false; + DBUG << "image size" << imageSize; + } + + int chunkSize = QByteArray(chunkSizeStart+constBinary.length(), chunkSizeEnd-(chunkSizeStart+constBinary.length())).toUInt(); + DBUG << "chunk size" << chunkSize; + + int startOfChunk=(chunkSizeEnd+1)-response.data.constData(); + if (startOfChunk+chunkSize > response.data.length()) { + DBUG << "Invalid chunk size"; + break; + } + + imageData.append(chunkSizeEnd+1, chunkSize); + dataToRead -= chunkSize; + } + + DBUG << dataToRead << imageData.size(); + emit albumArt(song, 0==dataToRead ? imageData : QByteArray()); +} /* * Data is written during idle. @@ -2124,9 +2201,7 @@ songs.clear(); } for (const QString &sub: subDirs) { - if (!recursivelyListDir(sub, songs)) { - return false; - } + recursivelyListDir(sub, songs); } if (topLevel && !songs.isEmpty()) { @@ -2522,7 +2597,7 @@ { "OK", false, MPDServerInfo::ForkedDaapd, "forked-daapd" } }; -void MPDServerInfo::detect(void) { +void MPDServerInfo::detect() { MPDConnection *conn; if (!isUndetermined()) { @@ -2593,7 +2668,7 @@ } } -void MPDServerInfo::reset(void) { +void MPDServerInfo::reset() { setServerType(MPDServerInfo::Undetermined); serverName = "undetermined"; topLevelLsinfo = "lsinfo"; diff -Nru cantata-2.3.2/mpd-interface/mpdconnection.h cantata-2.3.3/mpd-interface/mpdconnection.h --- cantata-2.3.2/mpd-interface/mpdconnection.h 2018-06-23 08:19:22.000000000 +0000 +++ cantata-2.3.3/mpd-interface/mpdconnection.h 2018-12-09 19:43:31.000000000 +0000 @@ -184,18 +184,18 @@ lsinfoCommand += "\""; }; - void reset(void); - void detect(void); + void reset(); + void detect(); - ServerType getServerType(void) const { return serverType;} - bool isUndetermined(void) const { return serverType == Undetermined; } - bool isMpd(void) const { return serverType == Mpd; } - bool isMopidy(void) const { return serverType == Mopidy; } - bool isForkedDaapd(void) const { return serverType == ForkedDaapd; } - bool isPlayQueueIdValid(void) const { return serverType != ForkedDaapd; } + ServerType getServerType() const { return serverType;} + bool isUndetermined() const { return serverType == Undetermined; } + bool isMpd() const { return serverType == Mpd; } + bool isMopidy() const { return serverType == Mopidy; } + bool isForkedDaapd() const { return serverType == ForkedDaapd; } + bool isPlayQueueIdValid() const { return serverType != ForkedDaapd; } - const QString &getServerName(void) const { return serverName;} - const QByteArray &getTopLevelLsinfo(void) const { return topLevelLsinfo; } + const QString &getServerName() const { return serverName;} + const QByteArray &getTopLevelLsinfo() const { return topLevelLsinfo; } private: void setServerType(ServerType newServerType) { serverType = newServerType; } @@ -273,6 +273,7 @@ bool originalDateTagSupported() const { return tagTypes.contains(QLatin1String("OriginalDate")); } bool modifiedFindSupported() const { return ver>=CANTATA_MAKE_VERSION(0, 19, 0); } bool replaygainSupported() const { return ver>=CANTATA_MAKE_VERSION(0, 16, 0); } + bool supportsCoverDownload() const { return ver>=CANTATA_MAKE_VERSION(0, 21, 0) && isMpd(); } bool localFilePlaybackSupported() const; bool stickersSupported() const { return canUseStickers; } @@ -280,10 +281,10 @@ static bool isPlaylist(const QString &file); int unmuteVolume() { return unmuteVol; } bool isMuted() { return -1!=unmuteVol; } - bool isMpd(void) const { return serverInfo.isMpd(); } - bool isMopidy(void) const { return serverInfo.isMopidy(); } - bool isForkedDaapd(void) const { return serverInfo.isForkedDaapd(); } - bool isPlayQueueIdValid(void) const { return serverInfo.isPlayQueueIdValid(); } + bool isMpd() const { return serverInfo.isMpd(); } + bool isMopidy() const { return serverInfo.isMopidy(); } + bool isForkedDaapd() const { return serverInfo.isForkedDaapd(); } + bool isPlayQueueIdValid() const { return serverInfo.isPlayQueueIdValid(); } void setVolumeFadeDuration(int f) { fadeDuration=f; } QString ipAddress() const { return details.isLocal() ? QString() : sock.address(); } @@ -340,6 +341,7 @@ void getStatus(); void getUrlHandlers(); void getTagTypes(); + void getCover(const Song &song); // Database void loadLibrary(); @@ -380,7 +382,7 @@ void setRating(const QStringList &files, quint8 val); void getRating(const QString &file); - void seek(); + void seek(qint32 offset=0); Q_SIGNALS: void connectionChanged(const MPDConnectionDetails &details); @@ -438,6 +440,8 @@ void ifaceIp(const QString &addr); + void albumArt(const Song &song, const QByteArray &data); + private Q_SLOTS: void idleDataReady(); void onSocketStateChanged(QAbstractSocket::SocketState socketState); diff -Nru cantata-2.3.2/mpd-interface/song.cpp cantata-2.3.3/mpd-interface/song.cpp --- cantata-2.3.2/mpd-interface/song.cpp 2018-06-07 19:04:41.000000000 +0000 +++ cantata-2.3.3/mpd-interface/song.cpp 2018-10-20 22:39:40.000000000 +0000 @@ -396,11 +396,7 @@ QString Song::displayAlbum(const QString &albumName, quint16 albumYear) { - QString d=albumYear>0 ? albumName+QLatin1String(" (")+QString::number(albumYear)+QLatin1Char(')') : albumName; - while (d.contains(") (")) { - d=d.replace(") (", ", "); - } - return d; + return albumYear>0 ? albumName+QLatin1String(" (")+QString::number(albumYear)+QLatin1Char(')') : albumName; } static QSet prefixesToIngore=QSet() << QLatin1String("The"); @@ -750,12 +746,13 @@ return onlineService(); } #endif - return albumArtist()+QLatin1Char(':')+albumId()+QLatin1Char(':')+QString::number(disc); + return albumArtist()+QLatin1Char(':')+albumId(); //+QLatin1Char(':')+QString::number(disc); } -static QString basic(const QString &str) +static QString basic(const QString &str, const QStringList &extraToStrip=QStringList()) { - QStringList toStrip=QStringList() << QLatin1String("ft. ") << QLatin1String("feat. ") << QLatin1String("featuring ") << QLatin1String("f. "); + QStringList toStrip=QStringList() << QLatin1String("ft. ") << QLatin1String("feat. ") << QLatin1String("featuring ") << QLatin1String("f. ") + << extraToStrip; QStringList prefixes=QStringList() << QLatin1String(" ") << QLatin1String(" (") << QLatin1String(" ["); for (const QString &s: toStrip) { @@ -780,7 +777,7 @@ QString Song::basicTitle() const { - return basic(title); + return basic(title, QStringList() << QLatin1String("prod. ") << QLatin1String("prod ") << QLatin1String("producer ") << QLatin1String("produced by ")); } QString Song::filePath(const QString &base) const diff -Nru cantata-2.3.2/playlists/rulesplaylists.cpp cantata-2.3.3/playlists/rulesplaylists.cpp --- cantata-2.3.2/playlists/rulesplaylists.cpp 2018-06-01 18:19:40.000000000 +0000 +++ cantata-2.3.3/playlists/rulesplaylists.cpp 2018-10-20 22:39:40.000000000 +0000 @@ -82,6 +82,7 @@ case Order_Date: return constDateKey; case Order_Genre: return constGenreKey; case Order_Rating: return constRatingKey; + case Order_Title: return constOrderKey; case Order_Age: return "Age"; default: case Order_Random: return "Random"; @@ -98,6 +99,7 @@ case Order_Date: return tr("Date"); case Order_Genre: return tr("Genre"); case Order_Rating: return tr("Rating"); + case Order_Title: return tr("Title"); case Order_Age: return tr("File Age"); default: case Order_Random: return tr("Random"); @@ -220,7 +222,8 @@ str << constMaxAgeKey << constKeyValSep << e.maxAge << '\n'; } if (Order_Random!=e.order) { - str << constOrderKey << constKeyValSep << orderStr(e.order) << '\n'; + str << constOrderKey << constKeyValSep << orderStr(e.order) << '\n' + << constOrderAscendingKey << constKeyValSep << (e.orderAscending ? "true" : "false") << '\n'; } for (const Rule &rule: e.rules) { if (!rule.isEmpty()) { diff -Nru cantata-2.3.2/playlists/rulesplaylists.h cantata-2.3.3/playlists/rulesplaylists.h --- cantata-2.3.2/playlists/rulesplaylists.h 2018-05-23 19:49:35.000000000 +0000 +++ cantata-2.3.3/playlists/rulesplaylists.h 2018-10-20 22:39:40.000000000 +0000 @@ -44,6 +44,7 @@ Order_Date, Order_Genre, Order_Rating, + Order_Title, Order_Age, Order_Random, diff -Nru cantata-2.3.2/playlists/smartplaylistspage.cpp cantata-2.3.3/playlists/smartplaylistspage.cpp --- cantata-2.3.2/playlists/smartplaylistspage.cpp 2018-06-24 15:13:29.000000000 +0000 +++ cantata-2.3.3/playlists/smartplaylistspage.cpp 2018-10-20 22:39:40.000000000 +0000 @@ -277,6 +277,14 @@ return sortAscending ? (c<0 || (c==0 && s10 || (c==0 && s10 || (c==0 && s1 >::ConstIterator it(groupedTracks.constBegin()); @@ -483,14 +492,18 @@ QTreeWidgetItem *item=view->topLevelItem(it.key()); if (it.value().ok) { item->setText(COL_TRACKGAIN, tr("%1 dB").arg(Utils::formatNumber(updatedTags.trackGain, 2))); - item->setText(COL_TRACKPEAK, Utils::formatNumber(updatedTags.trackPeak, 6)); + if (!Utils::equal(updatedTags.trackPeak, 0.0)) { + item->setText(COL_TRACKPEAK, Utils::formatNumber(updatedTags.trackPeak, 6)); + } } else { item->setText(COL_TRACKGAIN, tr("Failed")); item->setText(COL_TRACKPEAK, tr("Failed")); } if (s->albumValues().ok) { item->setText(COL_ALBUMGAIN, tr("%1 dB").arg(Utils::formatNumber(updatedTags.albumGain, 2))); - item->setText(COL_ALBUMPEAK, Utils::formatNumber(updatedTags.albumPeak, 6)); + if (!Utils::equal(updatedTags.albumPeak, 0.0)) { + item->setText(COL_ALBUMPEAK, Utils::formatNumber(updatedTags.albumPeak, 6)); + } } else { item->setText(COL_ALBUMGAIN, tr("Failed")); item->setText(COL_ALBUMPEAK, tr("Failed")); @@ -505,7 +518,7 @@ item->setToolTip(COL_TRACKGAIN, tr("Original: %1 dB").arg(Utils::formatNumber(t.trackGain, 2))); diff=true; } - if (!Utils::equal(t.trackPeak, updatedTags.trackPeak, 0.000001)) { + if (!Utils::equal(updatedTags.trackPeak, 0.0) && !Utils::equal(t.trackPeak, updatedTags.trackPeak, 0.000001)) { item->setFont(COL_TRACKPEAK, italic); item->setToolTip(COL_TRACKPEAK, tr("Original: %1").arg(Utils::formatNumber(t.trackPeak, 6))); diff=true; @@ -515,7 +528,7 @@ item->setToolTip(COL_ALBUMGAIN, tr("Original: %1 dB").arg(Utils::formatNumber(t.albumGain, 2))); diffAlbum=true; } - if (!Utils::equal(t.albumPeak, updatedTags.albumPeak, 0.000001)) { + if (!Utils::equal(updatedTags.albumPeak, 0.0) && !Utils::equal(t.albumPeak, updatedTags.albumPeak, 0.000001)) { item->setFont(COL_ALBUMPEAK, italic); item->setToolTip(COL_ALBUMPEAK, tr("Original: %1").arg(Utils::formatNumber(t.albumPeak, 6))); diffAlbum=true; @@ -573,9 +586,13 @@ return; } item->setText(COL_TRACKGAIN, tr("%1 dB").arg(Utils::formatNumber(tags.trackGain, 2))); - item->setText(COL_TRACKPEAK, Utils::formatNumber(tags.trackPeak, 6)); + if (!Utils::equal(tags.trackPeak, 0.0)) { + item->setText(COL_TRACKPEAK, Utils::formatNumber(tags.trackPeak, 6)); + } item->setText(COL_ALBUMGAIN, tr("%1 dB").arg(Utils::formatNumber(tags.albumGain, 2))); - item->setText(COL_ALBUMPEAK, Utils::formatNumber(tags.albumPeak, 6)); + if (!Utils::equal(tags.albumPeak, 0.0)) { + item->setText(COL_ALBUMPEAK, Utils::formatNumber(tags.albumPeak, 6)); + } } } } diff -Nru cantata-2.3.2/support/Cantata-FontAwesome-README cantata-2.3.3/support/Cantata-FontAwesome-README --- cantata-2.3.2/support/Cantata-FontAwesome-README 1970-01-01 00:00:00.000000000 +0000 +++ cantata-2.3.3/support/Cantata-FontAwesome-README 2018-09-02 09:15:59.000000000 +0000 @@ -0,0 +1,14 @@ +Cantata-FontAwesome.ttf is a modified version of FontAwesome 4.7 The font file +has been modified so that the family name is now 'Cantata-FontAwesome' + +FontAwesome 5.0 is not compatible with FontAwesome 4.x - if a system were to +have FontAwesome 5.0 installed, and Cantata used the system installed font, +then Cantata would have **many** missing icons. Therefore, the only way to +ensure Cantata **always** loads a 4.7 FontAwesome was to bundle a local version +whose name had been changed to be Cantata specific. + +FontAwesome is licensed using SIL OFL 1.1 A copy of this license is included +in this folder - OFL.txt + +https://fontawesome.com/v4.7.0/license/ + diff -Nru cantata-2.3.2/support/monoicon.cpp cantata-2.3.3/support/monoicon.cpp --- cantata-2.3.2/support/monoicon.cpp 2018-06-01 18:03:40.000000000 +0000 +++ cantata-2.3.3/support/monoicon.cpp 2018-10-20 22:39:40.000000000 +0000 @@ -61,9 +61,9 @@ QColor col=QIcon::Selected==mode ? selectedColor : color; if (QIcon::Selected==mode && !col.isValid()) { #ifdef Q_OS_MAC - col=OSXStyle::self()->viewPalette().highlightedText().color(); + col=Utils::clampColor(OSXStyle::self()->viewPalette().highlightedText().color()); #else - col=QApplication::palette().highlightedText().color(); + col=Utils::clampColor(QApplication::palette().highlightedText().color()); #endif } QString key=(fileName.isEmpty() ? QString::number(fontAwesomeIcon) : fileName)+ diff -Nru cantata-2.3.2/support/OFL.txt cantata-2.3.3/support/OFL.txt --- cantata-2.3.2/support/OFL.txt 1970-01-01 00:00:00.000000000 +0000 +++ cantata-2.3.3/support/OFL.txt 2018-09-02 09:09:36.000000000 +0000 @@ -0,0 +1,97 @@ +Copyright (c) , (), +with Reserved Font Name . +Copyright (c) , (), +with Reserved Font Name . +Copyright (c) , (). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff -Nru cantata-2.3.2/support/utils.cpp cantata-2.3.3/support/utils.cpp --- cantata-2.3.2/support/utils.cpp 2018-05-21 16:24:47.000000000 +0000 +++ cantata-2.3.3/support/utils.cpp 2018-10-20 22:39:40.000000000 +0000 @@ -128,14 +128,26 @@ QString Utils::getFile(const QString &file) { - QString d(file); - int slashPos=d.lastIndexOf(constDirSep); + QString f(file); + int slashPos=f.lastIndexOf(constDirSep); if (-1!=slashPos) { - d.remove(0, slashPos+1); + f.remove(0, slashPos+1); } - return d; + return f; +} + +QString Utils::getExtension(const QString &file) +{ + QString f(file); + int dotPos=f.lastIndexOf('.'); + + if (-1!=dotPos) { + return f.mid(dotPos); + } + + return QString(); } QString Utils::changeExtension(const QString &file, const QString &extension) diff -Nru cantata-2.3.2/support/utils.h cantata-2.3.3/support/utils.h --- cantata-2.3.2/support/utils.h 2018-05-21 16:24:30.000000000 +0000 +++ cantata-2.3.3/support/utils.h 2018-10-20 22:39:40.000000000 +0000 @@ -65,6 +65,7 @@ extern QString getDir(const QString &file); extern QString getFile(const QString &file); + extern QString getExtension(const QString &file); extern QString changeExtension(const QString &file, const QString &extension); extern bool isDirReadable(const QString &dir); diff -Nru cantata-2.3.2/tags/tags.cpp cantata-2.3.3/tags/tags.cpp --- cantata-2.3.2/tags/tags.cpp 2018-05-17 16:25:37.000000000 +0000 +++ cantata-2.3.3/tags/tags.cpp 2018-08-27 08:48:33.000000000 +0000 @@ -82,6 +82,11 @@ #include #include +// loundess-scanner defines this as 5.0? But on tests with MPD 0.20.21 with FLAC and OPUS +// then using 0 made playback the same +// #define R128_RG_DIFF 5.0 +#define R128_RG_DIFF 0.0 + namespace Tags { @@ -105,19 +110,22 @@ return val.isEmpty() ? TagLib::String::null : TagLib::String(val.toUtf8().data(), TagLib::String::UTF8); } -static inline int convertToCantataRating(double r) { +static inline int convertToCantataRating(double r) +{ return qRound(r*10.0); //return qRound((r*10.0)/2.0); } -static std::string convertFromCantataRating(int rating) { +static std::string convertFromCantataRating(int rating) +{ std::stringstream ss; ss.precision(2); ss << std::fixed << (rating/10.0); return ss.str(); } -static double parseDoubleString(const TagLib::String &str) { +static double parseDoubleString(const TagLib::String &str) +{ if (str.isEmpty()) { return 0.0; } @@ -129,6 +137,19 @@ return ok ? v : 0.0; } +static int parseIntString(const TagLib::String &str) +{ + if (str.isEmpty()) { + return 0; + } + + QString s=tString2QString(str); + bool ok=false; + int v=s.toInt(&ok); + + return ok ? v : 0; +} + static double parseRgString(const TagLib::String &str) { if (str.isEmpty()) { @@ -165,7 +186,8 @@ struct RgTagsStrings { - RgTagsStrings(const RgTags &rg) { + RgTagsStrings(const RgTags &rg) + { std::stringstream ss; ss.precision(2); ss << std::fixed; @@ -705,8 +727,35 @@ } #endif -static void readVorbisCommentTags(TagLib::Ogg::XiphComment *tag, Song *song, ReplayGain *rg, QImage *img, QString *lyrics, int *rating) +#ifdef TAGLIB_OPUS_FOUND +static int16_t toOpusGain(double gain) +{ + gain = 256 * gain + 0.5; + return gain < -32768 + ? -32768 + : gain < 32767 ? static_cast(floor(gain)) : 32767; +} + +static double fromOpusGain(int16_t gain) +{ + return (double)gain/256.0; +} + +static std::string toString(int16_t val) { + std::stringstream ss; + ss << val; + return ss.str(); +} + +#endif + +static void readVorbisCommentTags(TagLib::Ogg::XiphComment *tag, Song *song, ReplayGain *rg, QImage *img, QString *lyrics, int *rating, TagLib::Ogg::File *file=nullptr) +{ + #ifndef TAGLIB_OPUS_FOUND + Q_UNUSED(file) + #endif + DBUG; if (song) { TagLib::String str=readVorbisTag(tag, "ALBUMARTIST"); @@ -734,10 +783,27 @@ } if (rg) { - rg->trackGain=parseRgString(readVorbisTag(tag, "REPLAYGAIN_TRACK_GAIN")); - rg->trackPeak=parseRgString(readVorbisTag(tag, "REPLAYGAIN_TRACK_PEAK")); - rg->albumGain=parseRgString(readVorbisTag(tag, "REPLAYGAIN_ALBUM_GAIN")); - rg->albumPeak=parseRgString(readVorbisTag(tag, "REPLAYGAIN_ALBUM_PEAK")); + #ifdef TAGLIB_OPUS_FOUND + if (file) { + TagLib::ByteVector header = static_cast(file)->packet(0); + int16_t opusHeaderGain = header.toShort(16, false); + DBUG << "opusHeaderGain" << opusHeaderGain; + rg->trackGain=fromOpusGain(parseIntString(readVorbisTag(tag, "R128_TRACK_GAIN")) + opusHeaderGain); + rg->albumGain=fromOpusGain(parseIntString(readVorbisTag(tag, "R128_ALBUM_GAIN")) + opusHeaderGain); + if (!Utils::equal(rg->trackGain, 0, 0.0000001) || !Utils::equal(rg->albumGain, 0, 0.0000001)) + { + rg->trackGain+=R128_RG_DIFF; + rg->albumGain+=R128_RG_DIFF; + } + rg->trackPeak=rg->albumPeak=0.0; + } else + #endif + { + rg->trackGain=parseRgString(readVorbisTag(tag, "REPLAYGAIN_TRACK_GAIN")); + rg->trackPeak=parseRgString(readVorbisTag(tag, "REPLAYGAIN_TRACK_PEAK")); + rg->albumGain=parseRgString(readVorbisTag(tag, "REPLAYGAIN_ALBUM_GAIN")); + rg->albumPeak=parseRgString(readVorbisTag(tag, "REPLAYGAIN_ALBUM_PEAK")); + } } if (img) { @@ -813,8 +879,12 @@ return false; } -static bool writeVorbisCommentTags(TagLib::Ogg::XiphComment *tag, const Song &from, const Song &to, const RgTags &rg, const QByteArray &img, int rating) +static bool writeVorbisCommentTags(TagLib::Ogg::XiphComment *tag, const Song &from, const Song &to, const RgTags &rg, const QByteArray &img, int rating, TagLib::Ogg::File *file=nullptr) { + #ifndef TAGLIB_OPUS_FOUND + Q_UNUSED(file) + #endif + DBUG; bool changed=false; @@ -842,15 +912,56 @@ } if (!rg.null) { - RgTagsStrings rgs(rg); - tag->addField("REPLAYGAIN_TRACK_GAIN", rgs.trackGain); - tag->addField("REPLAYGAIN_TRACK_PEAK", rgs.trackPeak); - if (rg.albumMode) { - tag->addField("REPLAYGAIN_ALBUM_GAIN", rgs.albumGain); - tag->addField("REPLAYGAIN_ALBUM_PEAK", rgs.albumPeak); - } else { + #ifdef TAGLIB_OPUS_FOUND + if (file) { + TagLib::ByteVector header = static_cast(file)->packet(0); + double opusHeaderGainCurrent = header.toShort(16, false) / 256.0; + +#if 0 + double opusHeaderGain = opusHeaderGainCurrent + (rg.albumMode ? rg.albumGain : rg.trackGain); // - 5.0; + int16_t opusHeaderGainInt = toOpusGain(opusHeaderGain); + double opusCorrectionDb = opusHeaderGainCurrent - (opusHeaderGainInt / 256.0); + + DBUG << "opusHeaderGain" << opusHeaderGainCurrent << opusHeaderGain << opusHeaderGainInt; + + header[16] = static_cast(static_cast(opusHeaderGainInt) & 0xff); + header[17] = static_cast(static_cast(opusHeaderGainInt) >> 8); + + static_cast(file)->setPacket(0, header); + + tag->addField("R128_TRACK_GAIN", toString(toOpusGain(rg.trackGain + opusCorrectionDb))); + if (rg.albumMode) { + tag->addField("R128_ALBUM_GAIN", toString(toOpusGain(rg.albumGain + opusCorrectionDb))); + } else { + tag->removeField("R128_ALBUM_GAIN"); + } +#else + // R128 Uses reference level of 28, but replaygain -18, subtract 5 to make equal + tag->addField("R128_TRACK_GAIN", toString(toOpusGain(rg.trackGain + opusHeaderGainCurrent - R128_RG_DIFF))); + if (rg.albumMode) { + tag->addField("R128_ALBUM_GAIN", toString(toOpusGain(rg.albumGain + opusHeaderGainCurrent - R128_RG_DIFF))); + } else { + tag->removeField("R128_ALBUM_GAIN"); + } +#endif + + tag->removeField("REPLAYGAIN_TRACK_GAIN"); + tag->removeField("REPLAYGAIN_TRACK_PEAK"); tag->removeField("REPLAYGAIN_ALBUM_GAIN"); tag->removeField("REPLAYGAIN_ALBUM_PEAK"); + } else + #endif + { + RgTagsStrings rgs(rg); + tag->addField("REPLAYGAIN_TRACK_GAIN", rgs.trackGain); + tag->addField("REPLAYGAIN_TRACK_PEAK", rgs.trackPeak); + if (rg.albumMode) { + tag->addField("REPLAYGAIN_ALBUM_GAIN", rgs.albumGain); + tag->addField("REPLAYGAIN_ALBUM_PEAK", rgs.albumPeak); + } else { + tag->removeField("REPLAYGAIN_ALBUM_GAIN"); + tag->removeField("REPLAYGAIN_ALBUM_PEAK"); + } } changed=true; } @@ -1146,7 +1257,7 @@ #ifdef TAGLIB_OPUS_FOUND } else if (TagLib::Ogg::Opus::File *file = dynamic_cast< TagLib::Ogg::Opus::File * >(fileref.file())) { if (file->tag()) { - readVorbisCommentTags(file->tag(), song, rg, img, lyrics, rating); + readVorbisCommentTags(file->tag(), song, rg, img, lyrics, rating, file); } #endif } else if (TagLib::FLAC::File *file = dynamic_cast< TagLib::FLAC::File * >(fileref.file())) { @@ -1267,7 +1378,7 @@ #ifdef TAGLIB_OPUS_FOUND } else if (TagLib::Ogg::Opus::File *file = dynamic_cast< TagLib::Ogg::Opus::File * >(fileref.file())) { if (file->tag()) { - changed=writeVorbisCommentTags(file->tag(), from, to, rg, img, rating) || changed; + changed=writeVorbisCommentTags(file->tag(), from, to, rg, img, rating, file) || changed; } #endif } else if (TagLib::FLAC::File *file = dynamic_cast< TagLib::FLAC::File * >(fileref.file())) { diff -Nru cantata-2.3.2/translations/cantata_fr.ts cantata-2.3.3/translations/cantata_fr.ts --- cantata-2.3.2/translations/cantata_fr.ts 2018-06-15 16:46:14.000000000 +0000 +++ cantata-2.3.3/translations/cantata_fr.ts 2018-09-01 22:32:49.000000000 +0000 @@ -1115,11 +1115,11 @@ No suitable ssh-askpass application installed! This is required for entering passwords. - Aucune application ssh-sakpass est disponible ! Cette dépendance est requise pour la saisie du mot de passe. + Aucune application ssh-sakpass n'est disponible ! Cette dépendance est requise pour la saisie du mot de passe. Mount point ("%1") is not empty! - Le point de montage ("%1") nest pas vide ! + Le point de montage ("%1") n'est pas vide ! "sshfs" is not installed! @@ -3641,7 +3641,7 @@ i18n: file: devices/remotedevicepropertieswidget.ui:360 i18n: ectx: property (text), widget (PlainNoteLabel, label_5) - Aucune application ssh-sakpass est disponible ! Cette dépendance est requise pour la saisie du mot de passe. + Aucune application ssh-sakpass n'est disponible ! Cette dépendance est requise pour la saisie du mot de passe. Name of Dynamic Rules @@ -8903,7 +8903,7 @@ Please refer to <a href="https://github.com/CDrummond/cantata/issues">Cantata's issue tracker</a> for a list of known issues, and to report new issues. - Visitez <a href="https://github.com/CDrummond/cantata/issues">le tracker de bug de Cantata,</a> pour une liste de bugs connus ou pour en reporter de nouveuax. + Visitez <a href="https://github.com/CDrummond/cantata/issues">le tracker de bug de Cantata,</a> pour une liste de bugs connus ou pour en reporter de nouveaux. @@ -12162,7 +12162,7 @@ Due to the way sshfs works, a suitable ssh-askpass application (ksshaskpass, ssh-askpass-gnome, etc.) will be required to enter the password. - Aucune application ssh-sakpass est disponible ! Cette dépendance est requise pour la saisie du mot de passe. + Aucune application ssh-sakpass n'est disponible ! Cette dépendance est requise pour la saisie du mot de passe. @@ -12214,12 +12214,12 @@ No suitable ssh-askpass application installed! This is required for entering passwords. - Aucune application ssh-sakpass est disponible ! Cette dépendance est requise pour la saisie du mot de passe. + Aucune application ssh-sakpass n'est disponible ! Cette dépendance est requise pour la saisie du mot de passe. Mount point ("%1") is not empty! - Le point de montage ("%1") nest pas vide ! + Le point de montage ("%1") n'est pas vide ! diff -Nru cantata-2.3.2/translations/cantata_it.ts cantata-2.3.3/translations/cantata_it.ts --- cantata-2.3.2/translations/cantata_it.ts 2018-06-15 16:46:14.000000000 +0000 +++ cantata-2.3.3/translations/cantata_it.ts 2018-12-09 20:38:54.000000000 +0000 @@ -6,7 +6,7 @@ Calculating size of files to be copied, please wait... - Calcolando la dimensione dei file da coipare, attendere... + Calcolando la dimensione dei file da copiare, attendere... @@ -85,7 +85,7 @@ The songs will need to be transcoded to a smaller filesize in order to be successfully copied. Non c'è abbastanza spazio disponibile sul dispositivo di destinazione. -I brani selezionati occupano %1, ma si sono solamente %2 disponibili. +I brani selezionati occupano %1, ma ci sono solamente %2 disponibili. I brani dovranno essere trascodificati ad una dimensione di file inferiore per poter venire copiati. @@ -95,7 +95,7 @@ The selected songs consume %1, but there is only %2 left. Non c'è abbastanza spazio disponibile sulla destinazione. -I brani selezionati occupano %1, ma si sono solamente %2 disponibili. +I brani selezionati occupano %1, ma ci sono solamente %2 disponibili. @@ -677,7 +677,7 @@ Cantata caches various pieces of information (covers, lyrics, etc). Below is a summary of Cantata's current cache usage. - Cantata memorizza in cache diverse informazioni (coperine, testi, ecc). Qua sotto c'è un riassuto dell'attuale utilizzo di cache di Cantata. + Cantata memorizza nella cache diverse informazioni (coperine, testi, ecc). Qua sotto c'è un riassuto dell'attuale utilizzo di cache di Cantata. @@ -1089,7 +1089,7 @@ To have Cantata call external commands (e.g. to edit tags with another application), add an entry for the command below. When at least one command command is defined, a 'Custom Actions' entry will be added to the context menus in the Library, Folders, and Playlists views. - Per far chiamare dei comandi esterni a Cantata (es. per modificare i tag con un'altra applicazione), aggiungere una voce per il comando sottostante. Quando viene definito almeno un comando, una voce 'Azioni Personalizzate' verrà aggiunta ai menu contestuali nelle viste Libreria, Cartelle e Scalette. + Per richiamare dei comandi esterni a Cantata (es. per modificare i tag con un'altra applicazione), aggiungere una voce per il comando sottostante. Quando viene definito almeno un comando, una voce 'Azioni Personalizzate' verrà aggiunta ai menu contestuali nelle viste Libreria, Cartelle e Scalette. @@ -1501,12 +1501,12 @@ You do not have an active subscription - Non possieti una sottoscrizione attiva + Non possiedi una sottoscrizione attiva Logged in (expiry:%1) - Regostrato (scadenza: %1) + Registrato (scadenza: %1) @@ -2027,7 +2027,7 @@ Filter On Genre - FIltra sul Genere + Filtra per Genere @@ -2754,12 +2754,12 @@ If no setting is specified for 'Filename', then Cantata will use a default of <code>cover</code>. This filename is used when downloading covers, or when adding music to your library from devices. - Se non è specificato nulla su 'Nome file', allora cantata userà <code>cover</code> come nome predefinito. Questo nome viene usato quando si scaricano le copertine, oppure di aggiunge della musica alla tua libreria dai dispositivi. + Se non è specificato nulla su 'Nome file', allora Cantata userà <code>cover</code> come nome predefinito. Questo nome viene usato quando si scaricano le copertine, oppure di aggiunge della musica alla tua libreria dai dispositivi. If no setting is specified for 'Filename', then Cantata will use a default of <code>cover</code>. This filename is used when downloading covers. - Se non è specificato nulla su 'Nome file', allora cantata userà <code>cover</code> come nome predefinito. Questo nome viene usato quando si scaricano le copertine. + Se non è specificato nulla su 'Nome file', allora Cantata userà <code>cover</code> come nome predefinito. Questo nome viene usato quando si scaricano le copertine. @@ -2848,7 +2848,7 @@ Click on the button, then enter the shortcut like you would in the program. Example for Ctrl+a: hold the Ctrl key and press a. - Clicca sul pulsante, quindi inserisci la scorciatoria che vorresti nel programma. + Clicca sul pulsante, quindi inserisci la scorciatoia che vorresti nel programma. Esempio per Ctrl+a: teni premuto il tasto Ctrl e premi a. @@ -2891,19 +2891,19 @@ Shortcut Conflict - Conflitto di Scorciatorie + Conflitto di Scorciatoie The "%1" shortcut is already in use, and cannot be configured. Please choose another one. - La scorciatoria "%1" è già in uso e non può essere configurata. + La scorciatoia "%1" è già in uso e non può essere configurata. Prego sceglierne un'altra. The "%1" shortcut is ambiguous with the shortcut for the following action: - La scorciatoria "%1" si può confondere con la scorciatoria per la seguente azione: + La scorciatoia "%1" si può confondere con la scorciatoia per la seguente azione: @@ -3285,7 +3285,7 @@ About Cantata... - Informazoni su Cantata... + Informazioni su Cantata... @@ -3479,7 +3479,7 @@ Collapse All - Contrai Tutto + Riduci Tutto @@ -3604,7 +3604,7 @@ Please refer to <a href="https://github.com/CDrummond/cantata/issues">Cantata's issue tracker</a> for a list of known issues, and to report new issues. - Per la lista dei problemi conosciut e per riportarne di nuovi, prego fare riferimento all'issue tracker di Cantata <a href="https://github.com/CDrummond/cantata/issues"> + Per la lista dei problemi conosciuti e per riportarne di nuovi, prego fare riferimento all'issue tracker di Cantata <a href="https://github.com/CDrummond/cantata/issues"> @@ -4444,7 +4444,7 @@ If you press and hold the stop button, then a menu will be shown allowing you to choose whether to stop playback now, or after the current track. (The stop button can be enabled in the Interface/Toolbar section) - Se prmi e teini premuto il pulsante di stop, apparirà un menu da cui potrai scegliere se fermare la riproduzione ora oppure dopo la traccia attuale. (È possibile abilitare il pulsante di stop nella sezione Interfaccia/Barra degli strumenti + Se premi e tieni premuto il pulsante di stop, apparirà un menu da cui potrai scegliere se fermare la riproduzione ora oppure dopo la traccia attuale. (È possibile abilitare il pulsante di stop nella sezione Interfaccia/Barra degli strumenti @@ -4916,7 +4916,7 @@ Manual podcast URL - URL maniale del podcast + URL manuale del podcast @@ -5189,7 +5189,7 @@ Delete Downloaded Episodes - Candella gli Episodi Scaricati + Cancella gli Episodi Scaricati @@ -6002,7 +6002,7 @@ Parse error loading cache file, please check your songs tags. - Errore di lettura caricandi il file di cache, prego verifica i tag dei tuoi brani. + Errore di lettura durante il caricament del file di cache, prego verifica i tag dei tuoi brani. @@ -6412,7 +6412,7 @@ song-dialog - Song dialogs (tags, replaygain, organiser) - song-dialogs - Finestre di dialogo delle canzoni (tag, replaygain, organinizza) + song-dialogs - Finestre di dialogo delle canzoni (tag, replaygain, organizza) diff -Nru cantata-2.3.2/widgets/groupedview.cpp cantata-2.3.3/widgets/groupedview.cpp --- cantata-2.3.2/widgets/groupedview.cpp 2018-03-07 19:04:41.000000000 +0000 +++ cantata-2.3.3/widgets/groupedview.cpp 2018-10-20 22:39:40.000000000 +0000 @@ -381,7 +381,7 @@ } int td=index.data(Cantata::Role_AlbumDuration).toUInt(); - QString totalDuration=td>0 ? Utils::formatTime(td) : QString(); + QString totalDuration=td>0 && td!=song.time ? Utils::formatTime(td) : QString(); QRect duratioRect(r.x(), r.y(), r.width(), textHeight); int totalDurationWidth=fm.width(totalDuration)+8; QRect textRect(r.x(), r.y(), r.width()-(rtl ? (4*constBorder) : totalDurationWidth), textHeight); diff -Nru cantata-2.3.2/widgets/icons.cpp cantata-2.3.3/widgets/icons.cpp --- cantata-2.3.2/widgets/icons.cpp 2018-05-24 17:57:53.000000000 +0000 +++ cantata-2.3.3/widgets/icons.cpp 2018-10-20 22:39:40.000000000 +0000 @@ -137,11 +137,10 @@ searchTabIcon=MonoIcon::icon(QLatin1String(":sidebar-search"), iconCol); } -void Icons::initToolbarIcons(QColor toolbarText) +void Icons::initToolbarIcons(const QColor &toolbarText) { bool rtl=QApplication::isRightToLeft(); - toolbarText=Utils::clampColor(toolbarText); toolbarPrevIcon=MonoIcon::icon(QLatin1String(rtl ? ":media-next" : ":media-prev"), toolbarText); toolbarPlayIcon=MonoIcon::icon(QLatin1String(rtl ? ":media-play-rtl" : ":media-play"), toolbarText); toolbarPauseIcon=MonoIcon::icon(QLatin1String(":media-pause"), toolbarText); diff -Nru cantata-2.3.2/widgets/icons.h cantata-2.3.3/widgets/icons.h --- cantata-2.3.2/widgets/icons.h 2018-05-24 17:45:02.000000000 +0000 +++ cantata-2.3.3/widgets/icons.h 2018-10-20 22:39:40.000000000 +0000 @@ -34,7 +34,7 @@ Icons(); void initSidebarIcons(); - void initToolbarIcons(QColor toolbarText); + void initToolbarIcons(const QColor &toolbarText); QIcon appIcon; QIcon genreIcon; QIcon artistIcon; diff -Nru cantata-2.3.2/widgets/nowplayingwidget.cpp cantata-2.3.3/widgets/nowplayingwidget.cpp --- cantata-2.3.2/widgets/nowplayingwidget.cpp 2018-05-14 21:28:49.000000000 +0000 +++ cantata-2.3.3/widgets/nowplayingwidget.cpp 2018-10-20 22:39:40.000000000 +0000 @@ -491,12 +491,23 @@ ensurePolished(); QToolButton btn(this); btn.ensurePolished(); - track->setPalette(btn.palette()); - artist->setPalette(btn.palette()); - time->setPalette(btn.palette()); - slider->updateStyleSheet(track->palette().windowText().color()); - ratingWidget->setColor(Utils::clampColor(track->palette().buttonText().color())); - infoLabel->setPalette(btn.palette()); + auto pal = btn.palette(); + auto col = Utils::clampColor(pal.windowText().color()); + if (col==textColor()) { + return; // No change + } + + for (auto group: {QPalette::Inactive, QPalette::Active}) { + for (auto role: {QPalette::WindowText, QPalette::ButtonText, QPalette::Text}) { + pal.setColor(group, role, col); + } + } + track->setPalette(pal); + artist->setPalette(pal); + time->setPalette(pal); + slider->updateStyleSheet(col); + ratingWidget->setColor(col); + infoLabel->setPalette(pal); } void NowPlayingWidget::resizeEvent(QResizeEvent *ev) diff -Nru cantata-2.3.2/widgets/volumecontrol.cpp cantata-2.3.3/widgets/volumecontrol.cpp --- cantata-2.3.2/widgets/volumecontrol.cpp 2018-03-07 19:04:41.000000000 +0000 +++ cantata-2.3.3/widgets/volumecontrol.cpp 2018-10-20 22:39:40.000000000 +0000 @@ -56,13 +56,18 @@ layout->addWidget(label); mpdVol->ensurePolished(); setFixedSize(mpdVol->width(), mpdVol->height()+(size*2)); - connect(mpdVol, SIGNAL(stateChanged()), SLOT(stateChanged())); connect(httpVol, SIGNAL(stateChanged()), SLOT(stateChanged())); mpdVol->setEnabled(true); httpVol->setEnabled(false); label->setAlignment(Qt::AlignCenter); label->setBold(false); - stateChanged(); + + stack->setCurrentIndex(0); + label->setCurrentIndex(0); + mpdVol->setActive(true); + httpVol->setActive(false); + label->setVisible(false); + connect(label, SIGNAL(activated(int)), SLOT(itemSelected(int))); QTimer::singleShot(500, this, SLOT(selectControl())); } @@ -107,15 +112,13 @@ void VolumeControl::stateChanged() { - stack->setCurrentIndex((sender()==httpVol && httpVol->isEnabled()) || - (sender()==mpdVol && !mpdVol->isEnabled() && httpVol->isEnabled()) ? 1 : 0); + stack->setCurrentIndex(httpVol->isEnabled() ? 1 : 0); mpdVol->setActive(0==stack->currentIndex()); httpVol->setActive(1==stack->currentIndex()); - label->setVisible(mpdVol->isEnabled() && httpVol->isEnabled()); + label->setVisible(httpVol->isEnabled()); label->blockSignals(true); label->setCurrentIndex(stack->currentIndex()); label->blockSignals(false); - setVisible(mpdVol->isEnabled() || httpVol->isEnabled()); } void VolumeControl::itemSelected(int i) diff -Nru cantata-2.3.2/widgets/volumeslider.cpp cantata-2.3.3/widgets/volumeslider.cpp --- cantata-2.3.2/widgets/volumeslider.cpp 2018-03-07 19:04:41.000000000 +0000 +++ cantata-2.3.3/widgets/volumeslider.cpp 2018-10-20 22:39:40.000000000 +0000 @@ -292,7 +292,6 @@ } bool wasEnabled=isEnabled(); setEnabled(volume>=0); - setVisible(volume>=0); update(); muteAction->setEnabled(isEnabled()); StdActions::self()->increaseVolumeAction->setEnabled(isEnabled());