diff --git a/SConstruct b/SConstruct index 7a48059c6bc..679a491f0f4 100644 --- a/SConstruct +++ b/SConstruct @@ -58,6 +58,7 @@ available_features = [features.Mad, features.LocaleCompare, features.Lilv, features.Battery, + features.MPRIS, # "Features" of dubious quality features.PerfTools, diff --git a/build/depends.py b/build/depends.py index 788157d9f0a..97d9e2e438b 100644 --- a/build/depends.py +++ b/build/depends.py @@ -499,6 +499,7 @@ def configure(self, build, conf): class TestHeaders(Dependence): def configure(self, build, conf): build.env.Append(CPPPATH="#lib/gtest-1.7.0/include") + build.env.Append(CPPPATH="#lib/gmock-1.7.0/include") class FidLib(Dependence): def sources(self, build): @@ -699,6 +700,13 @@ def configure(self, build, conf): raise Exception( "Could not find qtkeychain.") +class QtDBus(Dependence): + def configure(self, build, conf): + libs = ['Qt5DBus'] + if not conf.CheckLib(libs): + raise Exception('Couldn\'t find Qt5Dbus5 library.') + + class MixxxCore(Feature): def description(self): @@ -724,6 +732,16 @@ def sources(self, build): "control/controlttrotary.cpp", "control/controlencoder.cpp", + "broadcast/metadatabroadcast.cpp", + "broadcast/scrobblingmanager.cpp", + "broadcast/filelistener/filelistener.cpp", + "broadcast/filelistener/metadatafileworker.cpp", + "broadcast/listenbrainzlistener/networkrequest.cpp", + "broadcast/listenbrainzlistener/networkmanager.cpp", + "broadcast/listenbrainzlistener/networkreply.cpp", + "broadcast/listenbrainzlistener/listenbrainzservice.cpp", + "broadcast/listenbrainzlistener/listenbrainzjsonfactory.cpp", + "controllers/dlgcontrollerlearning.cpp", "controllers/dlgprefcontroller.cpp", "controllers/dlgprefcontrollers.cpp", @@ -737,6 +755,7 @@ def sources(self, build): "preferences/dialog/dlgprefeffects.cpp", "preferences/dialog/dlgprefeq.cpp", "preferences/dialog/dlgpreferences.cpp", + "preferences/dialog/dlgprefmetadata.cpp", "preferences/dialog/dlgprefinterface.cpp", "preferences/dialog/dlgpreflibrary.cpp", "preferences/dialog/dlgprefnovinyl.cpp", @@ -752,6 +771,8 @@ def sources(self, build): "preferences/broadcastsettingsmodel.cpp", "preferences/effectsettingsmodel.cpp", "preferences/broadcastprofile.cpp", + "preferences/metadatafilesettings.cpp", + "preferences/listenbrainzsettings.cpp", "preferences/upgrade.cpp", "preferences/dlgpreferencepage.cpp", @@ -1155,6 +1176,8 @@ def sources(self, build): "track/trackinfo.cpp", "track/trackrecord.cpp", "track/trackref.cpp", + "track/trackplaytimers.cpp", + "track/tracktiminginfo.cpp", "mixer/auxiliary.cpp", "mixer/baseplayer.cpp", @@ -1266,6 +1289,8 @@ def sources(self, build): 'preferences/dialog/dlgprefautodjdlg.ui', 'preferences/dialog/dlgprefbeatsdlg.ui', 'preferences/dialog/dlgprefdeckdlg.ui', + 'preferences/dialog/dlgprefmetadatadlg.ui', + 'preferences/dialog/dlgfilelistenerbox.ui', 'preferences/dialog/dlgprefcrossfaderdlg.ui', 'preferences/dialog/dlgpreflv2dlg.ui', 'preferences/dialog/dlgprefeffectsdlg.ui', @@ -1356,7 +1381,7 @@ def configure(self, build, conf): # http://clang.llvm.org/docs/ThreadSafetyAnalysis.html build.env.Append(CCFLAGS='-Wthread-safety') build.env.Append(CCFLAGS='-Wextra') - + build.env.Append(CCFLAGS='-Werror=return-type') # Always generate debugging info. build.env.Append(CCFLAGS='-g') elif build.toolchain_is_msvs: diff --git a/build/features.py b/build/features.py index 086750cad82..1dda49d4b13 100644 --- a/build/features.py +++ b/build/features.py @@ -1363,3 +1363,37 @@ def sources(self, build): def depends(self, build): return [depends.QtKeychain] + +class MPRIS(Feature): + def description(self): + return "MPRIS implementation using QtDbus" + + def enabled(self,build): + if build.platform_is_linux: + build.flags['mpris'] = util.get_flags(build.env, 'mpris', 1) + else: + build.flags['mpris'] = util.get_flags(build.env, 'mpris', 0) + if int(build.flags['mpris']): + return True + return False + + def add_options(self, build, vars): + vars.Add('mpris', 'Set to 1 to enable MPRIS implementation to broadcast current track and interact with Mixxx', 0) + + def configure(self,build,conf): + if build.platform_is_linux and self.enabled(build): + build.env.Append(CPPDEFINES='__MPRIS__') + + def sources(self,build): + if build.platform_is_linux: + return ["broadcast/mpris/mprisservice.cpp", + "broadcast/mpris/mpris.cpp", + "broadcast/mpris/mediaplayer2.cpp", + "broadcast/mpris/mediaplayer2player.cpp", + "broadcast/mpris/mprisplayer.cpp", + "broadcast/mpris/mediaplayer2playlists.cpp", + "broadcast/mpris/mediaplayer2tracklist.cpp"] + return [] + def depends(self,build): + return [depends.QtDBus] + diff --git a/src/broadcast/broadcastmanager.cpp b/src/broadcast/broadcastmanager.cpp index ff9f9d4d325..fd71a14922c 100644 --- a/src/broadcast/broadcastmanager.cpp +++ b/src/broadcast/broadcastmanager.cpp @@ -27,7 +27,7 @@ BroadcastManager::BroadcastManager(SettingsManager* pSettingsManager, m_pNetworkStream(pSoundManager->getNetworkStream()) { const bool persist = true; m_pBroadcastEnabled = new ControlPushButton( - ConfigKey(BROADCAST_PREF_KEY,"enabled"), persist); + ConfigKey(BROADCAST_PREF_KEY, "enabled"), persist); m_pBroadcastEnabled->setButtonMode(ControlPushButton::TOGGLE); connect(m_pBroadcastEnabled, SIGNAL(valueChanged(double)), this, SLOT(slotControlEnabled(double))); diff --git a/src/broadcast/filelistener/filelistener.cpp b/src/broadcast/filelistener/filelistener.cpp new file mode 100644 index 00000000000..0297ba13de4 --- /dev/null +++ b/src/broadcast/filelistener/filelistener.cpp @@ -0,0 +1,117 @@ + +#include +#include + +#include "broadcast/filelistener/filelistener.h" +#include "broadcast/filelistener/metadatafileworker.h" +#include "preferences/metadatafilesettings.h" + + +FileListener::FileListener(UserSettingsPointer pConfig) + : m_COsettingsChanged(kFileSettingsChanged), + m_pConfig(pConfig), + m_latestSettings(MetadataFileSettings::getPersistedSettings(pConfig)), + m_filePathChanged(false), + m_tracksPaused(false) { + + MetadataFileWorker *newWorker = new MetadataFileWorker(m_latestSettings.filePath); + newWorker->moveToThread(&m_workerThread); + + connect(&m_workerThread, SIGNAL(finished()), + newWorker, SLOT(deleteLater())); + + connect(this, SIGNAL(deleteFile()), + newWorker, SLOT(slotDeleteFile())); + + connect(this, SIGNAL(moveFile(QString)), + newWorker, SLOT(slotMoveFile(QString))); + + connect(this, SIGNAL(writeMetadataToFile(QByteArray)), + newWorker, SLOT(slotWriteMetadataToFile(QByteArray))); + + connect(this, SIGNAL(clearFile()), + newWorker, SLOT(slotClearFile())); + + connect(&m_COsettingsChanged, SIGNAL(valueChanged(double)), + this, SLOT(slotFileSettingsChanged(double))); + + m_workerThread.start(); +} + +FileListener::~FileListener() { + emit clearFile(); + m_workerThread.quit(); + m_workerThread.wait(); +} + + +void FileListener::slotBroadcastCurrentTrack(TrackPointer pTrack) { + if (!m_latestSettings.enabled) { + return; + } + if (!pTrack) { + emit clearFile(); + } + else { + m_fileContents.title = pTrack->getTitle(); + m_fileContents.artist = pTrack->getArtist(); + QString writtenString(m_latestSettings.fileFormatString); + writtenString.replace("$author", pTrack->getArtist()). + replace("$title", pTrack->getTitle()) += '\n'; + QTextCodec *codec = QTextCodec::codecForName(m_latestSettings.fileEncoding); + DEBUG_ASSERT(codec); + QByteArray fileContents = codec->fromUnicode(writtenString); + m_tracksPaused = false; + emit writeMetadataToFile(fileContents); + } +} + +void FileListener::slotScrobbleTrack(TrackPointer pTrack) { + Q_UNUSED(pTrack); +} + +void FileListener::slotAllTracksPaused() { + if (!m_latestSettings.enabled) { + return; + } + m_tracksPaused = true; + emit clearFile(); +} + +void FileListener::slotFileSettingsChanged(double value) { + if (value) { + FileSettings latestSettings = MetadataFileSettings::getLatestSettings(); + m_filePathChanged = latestSettings.filePath != m_latestSettings.filePath; + m_latestSettings = latestSettings; + updateStateFromSettings(); + } +} + +void FileListener::updateStateFromSettings() { + if (m_latestSettings.enabled) { + updateFile(); + } + else { + emit deleteFile(); + } +} + +void FileListener::updateFile() { + if (m_filePathChanged) { + emit moveFile(m_latestSettings.filePath); + m_filePathChanged = false; + } + if (!m_tracksPaused && !m_fileContents.isEmpty()) { + QTextCodec *codec = QTextCodec::codecForName(m_latestSettings.fileEncoding); + if (!codec) { + qWarning() << "Text codec selected from metadata broadcast settings doesn't exist"; + codec = QTextCodec::codecForName("UTF-8"); + } + QString newContents(m_latestSettings.fileFormatString); + newContents.replace("$author", m_fileContents.artist) + .replace("$title", m_fileContents.title) += '\n'; + QByteArray contentsBinary = codec->fromUnicode(newContents); + emit writeMetadataToFile(contentsBinary); + } +} + diff --git a/src/broadcast/filelistener/filelistener.h b/src/broadcast/filelistener/filelistener.h new file mode 100644 index 00000000000..9c79bc1b7f2 --- /dev/null +++ b/src/broadcast/filelistener/filelistener.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include "preferences/dialog/dlgprefmetadata.h" +#include "control/controlpushbutton.h" +#include "broadcast/scrobblingservice.h" + +class FileListener: public ScrobblingService { + Q_OBJECT + public: + explicit FileListener(UserSettingsPointer pSettings); + ~FileListener() override; + void slotBroadcastCurrentTrack(TrackPointer pTrack) override; + void slotScrobbleTrack(TrackPointer pTrack) override; + void slotAllTracksPaused() override; + signals: + void deleteFile(); + void moveFile(QString destination); + void writeMetadataToFile(QByteArray contents); + void clearFile(); + private slots: + void slotFileSettingsChanged(double value); + private: + + struct WrittenMetadata { + QString title, artist; + bool isEmpty() { + return title.isEmpty() && artist.isEmpty(); + } + }; + + void updateStateFromSettings(); + void updateFile(); + + ControlPushButton m_COsettingsChanged; + UserSettingsPointer m_pConfig; + FileSettings m_latestSettings; + QThread m_workerThread; + WrittenMetadata m_fileContents; + bool m_filePathChanged; + bool m_tracksPaused; +}; \ No newline at end of file diff --git a/src/broadcast/filelistener/metadatafileworker.cpp b/src/broadcast/filelistener/metadatafileworker.cpp new file mode 100644 index 00000000000..c0ea4ec6d84 --- /dev/null +++ b/src/broadcast/filelistener/metadatafileworker.cpp @@ -0,0 +1,29 @@ + +#include "broadcast/filelistener/metadatafileworker.h" + +MetadataFileWorker::MetadataFileWorker(const QString& filePath) + : m_file(filePath) { +} + +void MetadataFileWorker::slotDeleteFile() { + m_file.remove(); +} + +void MetadataFileWorker::slotMoveFile(QString destination) { + m_file.remove(); + m_file.setFileName(destination); +} + +void MetadataFileWorker::slotWriteMetadataToFile(QByteArray fileContents) { + m_file.open(QIODevice::WriteOnly | + QIODevice::Text | + QIODevice::Unbuffered); + m_file.write(fileContents); + m_file.close(); +} + +void MetadataFileWorker::slotClearFile() { + m_file.resize(0); +} + + diff --git a/src/broadcast/filelistener/metadatafileworker.h b/src/broadcast/filelistener/metadatafileworker.h new file mode 100644 index 00000000000..b3a43e5cedb --- /dev/null +++ b/src/broadcast/filelistener/metadatafileworker.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +class MetadataFileWorker : public QObject { + Q_OBJECT + public: + explicit MetadataFileWorker(const QString& filePath); + public slots: + void slotDeleteFile(); + void slotMoveFile(QString destination); + void slotWriteMetadataToFile(QByteArray fileContents); + void slotClearFile(); + private: + QFile m_file; +}; + diff --git a/src/broadcast/listenbrainzlistener/listenbrainzjsonfactory.cpp b/src/broadcast/listenbrainzlistener/listenbrainzjsonfactory.cpp new file mode 100644 index 00000000000..bac2beacc60 --- /dev/null +++ b/src/broadcast/listenbrainzlistener/listenbrainzjsonfactory.cpp @@ -0,0 +1,38 @@ + +#include +#include +#include +#include + +#include "broadcast/listenbrainzlistener/listenbrainzjsonfactory.h" + + +QByteArray ListenBrainzJSONFactory::getJSONFromTrack(TrackPointer pTrack, JsonType type) { + QJsonObject jsonObject; + QString stringType; + if (type == NowListening) { + stringType = "playing_now"; + } + else { + stringType = "single"; + } + + QJsonArray payloadArray; + QJsonObject payloadObject; + QJsonObject metadataObject; + QString title = pTrack->getTitle(); + QString artist = pTrack->getArtist(); + metadataObject.insert("artist_name", artist); + metadataObject.insert("track_name", title); + payloadObject.insert("track_metadata", metadataObject); + qint64 timeStamp = QDateTime::currentMSecsSinceEpoch() / 1000; + + if (type == Single) { + payloadObject.insert("listened_at", timeStamp); + } + payloadArray.append(payloadObject); + jsonObject.insert("listen_type", stringType); + jsonObject.insert("payload", payloadArray); + QJsonDocument doc(jsonObject); + return doc.toJson(QJsonDocument::Compact); +} diff --git a/src/broadcast/listenbrainzlistener/listenbrainzjsonfactory.h b/src/broadcast/listenbrainzlistener/listenbrainzjsonfactory.h new file mode 100644 index 00000000000..8a011cae8e7 --- /dev/null +++ b/src/broadcast/listenbrainzlistener/listenbrainzjsonfactory.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include "track/track.h" + +namespace ListenBrainzJSONFactory { + enum JsonType {NowListening, Single}; + QByteArray getJSONFromTrack(TrackPointer pTrack, JsonType type); +}; \ No newline at end of file diff --git a/src/broadcast/listenbrainzlistener/listenbrainzservice.cpp b/src/broadcast/listenbrainzlistener/listenbrainzservice.cpp new file mode 100644 index 00000000000..1c02de7e838 --- /dev/null +++ b/src/broadcast/listenbrainzlistener/listenbrainzservice.cpp @@ -0,0 +1,62 @@ + +#include + +#include "preferences/listenbrainzsettings.h" +#include "broadcast/listenbrainzlistener/listenbrainzservice.h" +#include "broadcast/listenbrainzlistener/listenbrainzjsonfactory.h" +#include "broadcast/listenbrainzlistener/networkmanager.h" + +ListenBrainzService::ListenBrainzService(UserSettingsPointer pSettings) + : m_request(ListenBrainzAPIURL), + m_latestSettings(ListenBrainzSettingsManager::getPersistedSettings(pSettings)), + m_COSettingsChanged(kListenBrainzSettingsChanged) { + connect(&m_manager, &QNetworkAccessManager::finished, + this, &ListenBrainzService::slotAPICallFinished); + connect(&m_COSettingsChanged, &ControlPushButton::valueChanged, + this, &ListenBrainzService::slotSettingsChanged); + m_request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + if (m_latestSettings.enabled) { + m_request.setRawHeader("Authorization", "Token " + m_latestSettings.userToken.toUtf8()); + } +} + + +void ListenBrainzService::slotBroadcastCurrentTrack(TrackPointer pTrack) { + Q_UNUSED(pTrack); + /*if (!pTrack || !m_latestSettings.enabled) + return; + m_currentJSON = new QByteArray( + ListenBrainzJSONFactory::getJSONFromTrack( + pTrack, ListenBrainzJSONFactory::NowListening)); + m_manager.post(m_request, *m_currentJSON);*/ +} + +void ListenBrainzService::slotScrobbleTrack(TrackPointer pTrack) { + if (!pTrack || !m_latestSettings.enabled) + return; + m_currentJSON = + ListenBrainzJSONFactory::getJSONFromTrack( + pTrack, ListenBrainzJSONFactory::Single); + m_manager.post(m_request, m_currentJSON); +} + +void ListenBrainzService::slotAllTracksPaused() { + +} + +void ListenBrainzService::slotAPICallFinished(QNetworkReply *reply) { + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "API call to ListenBrainz error: " << + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + } + m_currentJSON.clear(); +} + +void ListenBrainzService::slotSettingsChanged(double value) { + if (value) { + m_latestSettings = ListenBrainzSettingsManager::getLatestSettings(); + if (m_latestSettings.enabled) { + m_request.setRawHeader("Authorization", "Token " + m_latestSettings.userToken.toUtf8()); + } + } +} \ No newline at end of file diff --git a/src/broadcast/listenbrainzlistener/listenbrainzservice.h b/src/broadcast/listenbrainzlistener/listenbrainzservice.h new file mode 100644 index 00000000000..66ba235849d --- /dev/null +++ b/src/broadcast/listenbrainzlistener/listenbrainzservice.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +#include "control/controlpushbutton.h" +#include "preferences/listenbrainzsettings.h" +#include "broadcast/scrobblingservice.h" +#include "broadcast/listenbrainzlistener/networkrequest.h" +#include "broadcast/listenbrainzlistener/networkreply.h" +#include "broadcast/listenbrainzlistener/networkmanager.h" + +class NetworkRequest; + +namespace { + const QUrl ListenBrainzAPIURL("https://api.listenbrainz.org/1/submit-listens"); + const QUrl MockServerURL("http://localhost/cgi-bin/mixxxPostDummy.py"); +} + +class ListenBrainzService : public ScrobblingService { + Q_OBJECT + public: + explicit ListenBrainzService(UserSettingsPointer pSettings); + public slots: + void slotBroadcastCurrentTrack(TrackPointer pTrack) override; + void slotScrobbleTrack(TrackPointer pTrack) override; + void slotAllTracksPaused() override; + private slots: + void slotAPICallFinished(QNetworkReply *reply); + void slotSettingsChanged(double value); + private: + QNetworkRequest m_request; + QNetworkAccessManager m_manager; + ListenBrainzSettings m_latestSettings; + ControlPushButton m_COSettingsChanged; + QByteArray m_currentJSON; +}; + diff --git a/src/broadcast/listenbrainzlistener/networkmanager.cpp b/src/broadcast/listenbrainzlistener/networkmanager.cpp new file mode 100644 index 00000000000..8d8bafbb5f4 --- /dev/null +++ b/src/broadcast/listenbrainzlistener/networkmanager.cpp @@ -0,0 +1,22 @@ + +#include + +#include "broadcast/listenbrainzlistener/networkmanager.h" +#include "broadcast/listenbrainzlistener/networkrequest.h" +#include "broadcast/listenbrainzlistener/networkreply.h" + + +NetworkReply* FakeNetworkManager::post(const NetworkRequest *request, const QByteArray& data) { + NetworkReply *reply = new FakeNetworkReply; + FakeNetworkReply *fakeReply = qobject_cast(reply); + fakeReply->setNetworkError(QNetworkReply::NoError); + fakeReply->setHttpError(200); + qDebug() << "Fake network manager sending POST request."; + qDebug() << "Headers:"; + for (auto header : request->rawHeaderList()) { + qDebug() << header; + } + qDebug() << "POST Body:"; + qDebug() << data; + return reply; +} \ No newline at end of file diff --git a/src/broadcast/listenbrainzlistener/networkmanager.h b/src/broadcast/listenbrainzlistener/networkmanager.h new file mode 100644 index 00000000000..48a20323be7 --- /dev/null +++ b/src/broadcast/listenbrainzlistener/networkmanager.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +class QByteArray; +class QNetworkReply; +class QNetworkRequest; +class NetworkReply; +class NetworkRequest; + +class NetworkManager : public QObject { + Q_OBJECT + public: + virtual NetworkReply *post(const NetworkRequest *request, const QByteArray& data) = 0; + signals: + void finished(NetworkReply *reply); +}; + +class FakeNetworkManager : public NetworkManager { + Q_OBJECT + public: + NetworkReply *post(const NetworkRequest *request, const QByteArray& data) override; +}; diff --git a/src/broadcast/listenbrainzlistener/networkreply.cpp b/src/broadcast/listenbrainzlistener/networkreply.cpp new file mode 100644 index 00000000000..32fa0db576a --- /dev/null +++ b/src/broadcast/listenbrainzlistener/networkreply.cpp @@ -0,0 +1,37 @@ +#include "broadcast/listenbrainzlistener/networkreply.h" + +QNetworkReply::NetworkError FakeNetworkReply::error() const { + return QNetworkReply::NoError; +} + +unsigned int FakeNetworkReply::getHttpError() { + return 200; +} + +QByteArray FakeNetworkReply::readAll() { + return QByteArray(); +} + +void FakeNetworkReply::setNetworkError(QNetworkReply::NetworkError error) { + netError = error; +} + +void FakeNetworkReply::setHttpError(unsigned int error) { + httpError = error; +} + +void FakeNetworkReply::setContents(QByteArray contents) { + this->contents = contents; +} + +QNetworkReply::NetworkError QtNetworkReply::error() const { + return QNetworkReply::NoError; +} + +unsigned int QtNetworkReply::getHttpError() { + return 200; +} + +QByteArray QtNetworkReply::readAll() { + return QByteArray(); +} diff --git a/src/broadcast/listenbrainzlistener/networkreply.h b/src/broadcast/listenbrainzlistener/networkreply.h new file mode 100644 index 00000000000..82400c9edf9 --- /dev/null +++ b/src/broadcast/listenbrainzlistener/networkreply.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +class NetworkReply : public QObject { + Q_OBJECT + public: + virtual QNetworkReply::NetworkError error() const = 0; + virtual unsigned int getHttpError() = 0; + virtual QByteArray readAll() = 0; + signals: + void finished(); +}; + +class FakeNetworkReply : public NetworkReply { + Q_OBJECT + public: + QNetworkReply::NetworkError error() const override; + unsigned int getHttpError() override; + QByteArray readAll() override; + + void setNetworkError(QNetworkReply::NetworkError error); + void setHttpError(unsigned int error); + void setContents(QByteArray contents); + private: + QByteArray contents; + QNetworkReply::NetworkError netError; + unsigned int httpError; +}; + +class QtNetworkReply : public NetworkReply { + Q_OBJECT + public: + QNetworkReply::NetworkError error() const override; + unsigned int getHttpError() override; + QByteArray readAll() override; +}; diff --git a/src/broadcast/listenbrainzlistener/networkrequest.cpp b/src/broadcast/listenbrainzlistener/networkrequest.cpp new file mode 100644 index 00000000000..e55c19546b5 --- /dev/null +++ b/src/broadcast/listenbrainzlistener/networkrequest.cpp @@ -0,0 +1,10 @@ +#include "broadcast/listenbrainzlistener/networkrequest.h" + + +void QtNetworkRequest::setRawHeader(const QByteArray& header, const QByteArray& value) { + m_request.setRawHeader(header, value); +} + +QList QtNetworkRequest::rawHeaderList() const { + return m_request.rawHeaderList(); +} diff --git a/src/broadcast/listenbrainzlistener/networkrequest.h b/src/broadcast/listenbrainzlistener/networkrequest.h new file mode 100644 index 00000000000..b6d4808973c --- /dev/null +++ b/src/broadcast/listenbrainzlistener/networkrequest.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +class NetworkRequest : public QObject { + Q_OBJECT + public: + explicit NetworkRequest(QUrl url) : m_url(std::move(url)) {} + virtual void setRawHeader(const QByteArray& header, const QByteArray& value) = 0; + virtual QList rawHeaderList() const = 0; + protected: + QUrl m_url; +}; + +class QtNetworkRequest : public NetworkRequest { + Q_OBJECT + public: + explicit QtNetworkRequest(QUrl url) : NetworkRequest(std::move(url)) {} + void setRawHeader(const QByteArray& header, const QByteArray& value) override; + + QList rawHeaderList() const override; + +private: + QNetworkRequest m_request; +}; + diff --git a/src/broadcast/metadatabroadcast.cpp b/src/broadcast/metadatabroadcast.cpp new file mode 100644 index 00000000000..0234b2c631c --- /dev/null +++ b/src/broadcast/metadatabroadcast.cpp @@ -0,0 +1,73 @@ + +#include + +#include "broadcast/metadatabroadcast.h" +#include "mixer/playerinfo.h" + +MetadataBroadcaster::MetadataBroadcaster() + : m_gracePeriodSeconds(5*60) { + +} + +void MetadataBroadcaster::slotAttemptScrobble(TrackPointer pTrack) { + if (m_trackedTracks.contains(pTrack->getId())) { + GracePeriod trackPeriod = m_trackedTracks.value(pTrack->getId()); + if ((trackPeriod.m_hasBeenEjected && + trackPeriod.m_msSinceEjection > + m_gracePeriodSeconds*1000.0) || + trackPeriod.m_firstTimeLoaded) { + for (auto& service : m_scrobblingServices) { + service->slotScrobbleTrack(pTrack); + } + trackPeriod.m_hasBeenEjected = false; + trackPeriod.m_firstTimeLoaded = false; + trackPeriod.m_timesTrackHasBeenScrobbled++; + } + } +} + +void MetadataBroadcaster::slotNowListening(TrackPointer pTrack) { + for (const auto& service : m_scrobblingServices) { + service->slotBroadcastCurrentTrack(pTrack); + } +} + +void MetadataBroadcaster::slotAllTracksPaused() { + for (const auto& service : m_scrobblingServices) { + service->slotAllTracksPaused(); + } +} + +MetadataBroadcasterInterface& MetadataBroadcaster::addNewScrobblingService + (const ScrobblingServicePtr& newService) { + m_scrobblingServices.push_back(newService); + return *this; +} + +void MetadataBroadcaster::newTrackLoaded(TrackPointer pTrack) { + if (!pTrack) + return; + if (!m_trackedTracks.contains(pTrack->getId())) { + m_trackedTracks.insert(pTrack->getId(), GracePeriod()); + } +} + +void MetadataBroadcaster::trackUnloaded(TrackPointer pTrack) { + if (!pTrack) + return; + if (m_trackedTracks.contains(pTrack->getId())) { + m_trackedTracks[pTrack->getId()].m_firstTimeLoaded = false; + m_trackedTracks[pTrack->getId()].m_hasBeenEjected = true; + m_trackedTracks[pTrack->getId()].m_msSinceEjection = 0.0; + } +} + +void MetadataBroadcaster::guiTick(double timeSinceLastTick) { + for (auto it : m_trackedTracks) { + if (it.m_hasBeenEjected) { + it.m_msSinceEjection += timeSinceLastTick; + } + } +} + + diff --git a/src/broadcast/metadatabroadcast.h b/src/broadcast/metadatabroadcast.h new file mode 100644 index 00000000000..c2b41859bab --- /dev/null +++ b/src/broadcast/metadatabroadcast.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include + +#include "broadcast/scrobblingservice.h" +#include "track/track.h" +#include "track/trackid.h" +#include "track/trackplaytimers.h" + +class MetadataBroadcasterInterface : public QObject { + Q_OBJECT + public slots: + virtual void slotNowListening(TrackPointer pTrack) = 0; + virtual void slotAttemptScrobble(TrackPointer pTrack) = 0; + virtual void slotAllTracksPaused() = 0; + public: + virtual ~MetadataBroadcasterInterface() = default; + virtual MetadataBroadcasterInterface& + addNewScrobblingService(const ScrobblingServicePtr& newService) = 0; + virtual void newTrackLoaded(TrackPointer pTrack) = 0; + virtual void trackUnloaded(TrackPointer pTrack) = 0; +}; + +class MetadataBroadcaster : public MetadataBroadcasterInterface { + Q_OBJECT + private: + struct GracePeriod { + double m_msSinceEjection; + unsigned int m_timesTrackHasBeenScrobbled = 0; + bool m_firstTimeLoaded = true; + bool m_hasBeenEjected = false; + GracePeriod() : + m_msSinceEjection(0.0) {} + }; + public: + + MetadataBroadcaster(); + MetadataBroadcasterInterface& + addNewScrobblingService(const ScrobblingServicePtr& newService) override; + void newTrackLoaded(TrackPointer pTrack) override; + void trackUnloaded(TrackPointer pTrack) override; + void slotNowListening(TrackPointer pTrack) override; + void slotAttemptScrobble(TrackPointer pTrack) override; + void slotAllTracksPaused() override; + void guiTick(double timeSinceLastTick); + + private: + unsigned int m_gracePeriodSeconds; + QHash m_trackedTracks; + QLinkedList m_scrobblingServices; +}; \ No newline at end of file diff --git a/src/broadcast/mpris/mediaplayer2.cpp b/src/broadcast/mpris/mediaplayer2.cpp new file mode 100644 index 00000000000..187054dee5e --- /dev/null +++ b/src/broadcast/mpris/mediaplayer2.cpp @@ -0,0 +1,66 @@ +#include +#include +#include + +#include "mediaplayer2.h" +#include "mixxx.h" +#include "sources/soundsourceproxy.h" + +MediaPlayer2::MediaPlayer2(MixxxMainWindow* pMixxx, QObject* parent) + : QDBusAbstractAdaptor(parent), + m_pMixxx(pMixxx) { +} + +bool MediaPlayer2::canQuit() const { + return true; +} + +bool MediaPlayer2::fullscreen() const { + return m_pMixxx->isFullScreen(); +} + +void MediaPlayer2::setFullscreen(bool fullscreen) { + m_pMixxx->slotViewFullScreen(fullscreen); +} + +bool MediaPlayer2::canSetFullscreen() const { + return true; +} + +bool MediaPlayer2::canRaise() const { + return true; +} + +bool MediaPlayer2::hasTrackList() const { + return false; +} + +QString MediaPlayer2::identity() const { + return "Mixxx"; +} + +QString MediaPlayer2::desktopEntry() const { + return "mixxx"; +} + +QStringList MediaPlayer2::supportedUriSchemes() const { + QStringList protocols; + protocols.append("file"); + return protocols; +} + +QStringList MediaPlayer2::supportedMimeTypes() const { + QStringList ret; + ret << "audio/mpeg" << "audio/ogg"; + return ret; +} + +void MediaPlayer2::Raise() { + m_pMixxx->raise(); +} + +void MediaPlayer2::Quit() { + QApplication::quit(); +} + + diff --git a/src/broadcast/mpris/mediaplayer2.h b/src/broadcast/mpris/mediaplayer2.h new file mode 100644 index 00000000000..226700f303c --- /dev/null +++ b/src/broadcast/mpris/mediaplayer2.h @@ -0,0 +1,50 @@ +#ifndef MEDIAPLAYER2_H +#define MEDIAPLAYER2_H + +#include +#include + +class MixxxMainWindow; + +// this implements the Version 2.2 of +// MPRIS D-Bus Interface Specification +// org.mpris.MediaPlayer2 +// http://specifications.freedesktop.org/mpris-spec/2.2/ + +class MediaPlayer2 : public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2") + Q_PROPERTY(bool CanQuit READ canQuit) + Q_PROPERTY(bool Fullscreen READ fullscreen WRITE setFullscreen) // optional + Q_PROPERTY(bool CanSetFullscreen READ canSetFullscreen) // optional + Q_PROPERTY(bool CanRaise READ canRaise) + Q_PROPERTY(bool HasTrackList READ hasTrackList) + Q_PROPERTY(QString Identity READ identity) + Q_PROPERTY(QString DesktopEntry READ desktopEntry) // optional + Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes) + Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes) + + public: + explicit MediaPlayer2(MixxxMainWindow* pMixxx, QObject* parent = 0); + + bool canQuit() const; + bool fullscreen() const; + void setFullscreen(bool fullscreen); + bool canSetFullscreen() const; + bool canRaise() const; + bool hasTrackList() const; + QString identity() const; + QString desktopEntry() const; + QStringList supportedUriSchemes() const; + QStringList supportedMimeTypes() const; + + public slots: + void Raise(); + void Quit(); + + private: + MixxxMainWindow* m_pMixxx; +}; + +#endif // MEDIAPLAYER2_H diff --git a/src/broadcast/mpris/mediaplayer2player.cpp b/src/broadcast/mpris/mediaplayer2player.cpp new file mode 100644 index 00000000000..9edd72064d5 --- /dev/null +++ b/src/broadcast/mpris/mediaplayer2player.cpp @@ -0,0 +1,130 @@ +#include +#include +#include +#include + +#include "broadcast/mpris/mediaplayer2player.h" + +MediaPlayer2Player::MediaPlayer2Player(PlayerManager *playerManager, + QObject *parent, + MixxxMainWindow *pWindow, + Mpris *pMpris, + UserSettingsPointer pSettings) + : QDBusAbstractAdaptor(parent), + m_mprisPlayer(playerManager, pWindow, pMpris, pSettings) +{ +} + +QString MediaPlayer2Player::playbackStatus() const { + return m_mprisPlayer.playbackStatus(); +} + +QString MediaPlayer2Player::loopStatus() const { + return m_mprisPlayer.loopStatus(); +} + +void MediaPlayer2Player::setLoopStatus(const QString& value) { + m_mprisPlayer.setLoopStatus(value); +} + +double MediaPlayer2Player::rate() const { + return m_mprisPlayer.rate(); +} + +void MediaPlayer2Player::setRate(double value) { + m_mprisPlayer.setRate(value); +} + +bool MediaPlayer2Player::shuffle() const { + return false; +} + +void MediaPlayer2Player::setShuffle(bool value) { + Q_UNUSED(value); +} + +QVariantMap MediaPlayer2Player::metadata() { + return m_mprisPlayer.metadata(); +} + +double MediaPlayer2Player::volume() const { + return m_mprisPlayer.volume(); +} + +void MediaPlayer2Player::setVolume(double value) { + m_mprisPlayer.setVolume(value); +} + +qlonglong MediaPlayer2Player::position() const { + return m_mprisPlayer.position(); +} + +double MediaPlayer2Player::minimumRate() const { + return -1.0; +} + +double MediaPlayer2Player::maximumRate() const { + return 1.0; +} + +bool MediaPlayer2Player::canGoNext() const { + return m_mprisPlayer.canGoNext(); +} + +bool MediaPlayer2Player::canGoPrevious() const { + return m_mprisPlayer.canGoPrevious(); +} + +bool MediaPlayer2Player::canPlay() const { + return m_mprisPlayer.canPlay(); +} + +bool MediaPlayer2Player::canPause() const { + return m_mprisPlayer.canPause(); +} + +bool MediaPlayer2Player::canSeek() const { + return m_mprisPlayer.canSeek(); +} + +bool MediaPlayer2Player::canControl() const { + return true; +} + +void MediaPlayer2Player::Next() { + m_mprisPlayer.nextTrack(); +} + +void MediaPlayer2Player::Previous() { + +} + +void MediaPlayer2Player::Pause() { + m_mprisPlayer.pause(); +} + +void MediaPlayer2Player::PlayPause() { + m_mprisPlayer.playPause(); +} + +void MediaPlayer2Player::Stop() { +} + +void MediaPlayer2Player::Play() { + m_mprisPlayer.play(); +} + +void MediaPlayer2Player::Seek(qlonglong offset) { + bool success; + qlonglong newPosition = m_mprisPlayer.seek(offset, success); +} + +void MediaPlayer2Player::SetPosition(const QDBusObjectPath& trackId, + qlonglong position) { + bool success; + qlonglong newPosition = m_mprisPlayer.setPosition(trackId, position, success); +} + +void MediaPlayer2Player::OpenUri(const QString& uri) { + m_mprisPlayer.openUri(uri); +} diff --git a/src/broadcast/mpris/mediaplayer2player.h b/src/broadcast/mpris/mediaplayer2player.h new file mode 100644 index 00000000000..a9d206024b6 --- /dev/null +++ b/src/broadcast/mpris/mediaplayer2player.h @@ -0,0 +1,86 @@ +#ifndef MEDIAPLAYER2PLAYER_H +#define MEDIAPLAYER2PLAYER_H + +#include +#include +#include +#include + +#include "broadcast/mpris/mprisplayer.h" +#include "control/controlproxy.h" + + +class AutoDJProcessor; +class PlayerManager; + +// this implements the Version 2.2 of +// MPRIS D-Bus Interface Specification +// org.mpris.MediaPlayer2.Player +// http://specifications.freedesktop.org/mpris-spec/2.2/ + +class MediaPlayer2Player : public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player") + Q_PROPERTY(QString PlaybackStatus READ playbackStatus) + Q_PROPERTY(QString LoopStatus READ loopStatus WRITE setLoopStatus) + Q_PROPERTY(double Rate READ rate WRITE setRate) + Q_PROPERTY(bool Shuffle READ shuffle WRITE setShuffle) // optional + Q_PROPERTY(QVariantMap Metadata READ metadata) + Q_PROPERTY(double Volume READ volume WRITE setVolume) + Q_PROPERTY(qlonglong Position READ position) + Q_PROPERTY(double MinimumRate READ minimumRate) + Q_PROPERTY(double MaximumRate READ maximumRate) + Q_PROPERTY(bool CanGoNext READ canGoNext) + Q_PROPERTY(bool CanGoPrevious READ canGoPrevious) + Q_PROPERTY(bool CanPlay READ canPlay) + Q_PROPERTY(bool CanPause READ canPause) + Q_PROPERTY(bool CanSeek READ canSeek) + Q_PROPERTY(bool CanControl READ canControl) + + public: + explicit MediaPlayer2Player(PlayerManager *playerManager, + QObject *parent, + MixxxMainWindow *pWindow, + Mpris *pMpris, + UserSettingsPointer pSettings); + + QString playbackStatus() const; + QString loopStatus() const; + void setLoopStatus(const QString& value); + double rate() const; + void setRate(double value); + bool shuffle() const; + void setShuffle(bool value); + QVariantMap metadata(); + double volume() const; + void setVolume(double value); + qlonglong position() const; + double minimumRate() const; + double maximumRate() const; + bool canGoNext() const; + bool canGoPrevious() const; + bool canPlay() const; + bool canPause() const; + bool canSeek() const; + bool canControl() const; + + public slots: + void Next(); + void Previous(); + void Pause(); + void PlayPause(); + void Stop(); + void Play(); + void Seek(qlonglong offset); + void SetPosition(const QDBusObjectPath& trackId, qlonglong position); + void OpenUri(const QString& uri); + + signals: + void Seeked(qlonglong position); + + private: + MprisPlayer m_mprisPlayer; +}; + +#endif // MEDIAPLAYER2PLAYER_H diff --git a/src/broadcast/mpris/mediaplayer2playlists.cpp b/src/broadcast/mpris/mediaplayer2playlists.cpp new file mode 100644 index 00000000000..a92cfa1254a --- /dev/null +++ b/src/broadcast/mpris/mediaplayer2playlists.cpp @@ -0,0 +1,42 @@ +#include + +#include "mediaplayer2playlists.h" + +MediaPlayer2Playlists::MediaPlayer2Playlists(QObject *parent) + : QDBusAbstractAdaptor(parent) { +} + +MediaPlayer2Playlists::~MediaPlayer2Playlists() { +} + +uint MediaPlayer2Playlists::playlistCount() const { + return 0; +} + +QStringList MediaPlayer2Playlists::orderings() const { + QStringList orderings; + return orderings; +} + +MaybePlaylist MediaPlayer2Playlists::activePlaylist() const { + MaybePlaylist activePlaylist; + activePlaylist.valid = false; + return activePlaylist; +} + +void MediaPlayer2Playlists::ActivatePlaylist(const QDBusObjectPath& playlistId) { + Q_UNUSED(playlistId); +} + +QList MediaPlayer2Playlists::GetPlaylists(uint index, + uint maxCount, + const QString& order, + bool reverseOrder) { + Q_UNUSED(index); + Q_UNUSED(maxCount); + Q_UNUSED(order); + Q_UNUSED(reverseOrder); + + QList playlists; + return playlists; +} diff --git a/src/broadcast/mpris/mediaplayer2playlists.h b/src/broadcast/mpris/mediaplayer2playlists.h new file mode 100644 index 00000000000..0db4efa2125 --- /dev/null +++ b/src/broadcast/mpris/mediaplayer2playlists.h @@ -0,0 +1,52 @@ +#ifndef MEDIAPLAYER2PLAYLIST_H +#define MEDIAPLAYER2PLAYLIST_H + +#include +#include +#include + +// this implements the Version 2.2 of +// MPRIS D-Bus Interface Specification +// org.mpris.MediaPlayer2.Playlists +// http://specifications.freedesktop.org/mpris-spec/2.2/ + + +typedef struct { + QDBusObjectPath o; + QString s1; + QString s2; +} Playlist; +Q_DECLARE_METATYPE(Playlist) + +typedef struct { + bool valid; + Playlist playlist; +} MaybePlaylist; + + +class MediaPlayer2Playlists : public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Playlists") + Q_PROPERTY(uint PlaylistCount READ playlistCount) + Q_PROPERTY(QStringList Orderings READ orderings) + Q_PROPERTY(MaybePlaylist ActivePlaylist READ activePlaylist) + + public: + MediaPlayer2Playlists(QObject* parent = 0); + virtual ~MediaPlayer2Playlists(); + + uint playlistCount() const; + QStringList orderings() const; + MaybePlaylist activePlaylist() const; + + public slots: + void ActivatePlaylist(const QDBusObjectPath& PlaylistId); + QList GetPlaylists(uint index, uint maxCount, const QString& order, + bool reverseOrder); + + signals: + void PlaylistChanged(const Playlist& playlist); +}; + +#endif // MEDIAPLAYER2PLAYLIST_H diff --git a/src/broadcast/mpris/mediaplayer2tracklist.cpp b/src/broadcast/mpris/mediaplayer2tracklist.cpp new file mode 100644 index 00000000000..93e74d88da1 --- /dev/null +++ b/src/broadcast/mpris/mediaplayer2tracklist.cpp @@ -0,0 +1,47 @@ +#include + +#include "mediaplayer2tracklist.h" + +MediaPlayer2TrackList::MediaPlayer2TrackList(QObject* parent) + : QDBusAbstractAdaptor(parent) { +} + +MediaPlayer2TrackList::~MediaPlayer2TrackList() { +} + +TrackIds MediaPlayer2TrackList::tracks() const { + TrackIds tracks; + return tracks; +} + +bool MediaPlayer2TrackList::canEditTracks() const { + return false; +} + +TrackMetadata MediaPlayer2TrackList::GetTracksMetadata( + const TrackIds& tracks) const { + Q_UNUSED(tracks); + + TrackMetadata metadata; + return metadata; +} + +void MediaPlayer2TrackList::AddTrack(const QString& uri, + const QDBusObjectPath& afterTrack, + bool setAsCurrent) { + Q_UNUSED(uri); + Q_UNUSED(afterTrack); + Q_UNUSED(setAsCurrent); +} + +void MediaPlayer2TrackList::RemoveTrack(const QDBusObjectPath& trackId) { + Q_UNUSED(trackId); +} + +void MediaPlayer2TrackList::GoTo(const QDBusObjectPath& trackId) { + Q_UNUSED(trackId); +} + + + + diff --git a/src/broadcast/mpris/mediaplayer2tracklist.h b/src/broadcast/mpris/mediaplayer2tracklist.h new file mode 100644 index 00000000000..2d805530d65 --- /dev/null +++ b/src/broadcast/mpris/mediaplayer2tracklist.h @@ -0,0 +1,44 @@ +#ifndef MEDIAPLAYER2TRACKLIST_H +#define MEDIAPLAYER2TRACKLIST_H + +#include +#include +#include + +// this implements the Version 2.2 of +// MPRIS D-Bus Interface Specification +// org.mpris.MediaPlayer2.TrackList +// http://specifications.freedesktop.org/mpris-spec/2.2/ + +typedef QList TrackMetadata; +Q_DECLARE_METATYPE(TrackMetadata) +typedef QList TrackIds; + +class MediaPlayer2TrackList : public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.TrackList") + Q_PROPERTY(TrackIds Tracks READ tracks) + Q_PROPERTY(bool CanEditTracks READ canEditTracks) + + public: + MediaPlayer2TrackList(QObject* parent = 0); + virtual ~MediaPlayer2TrackList(); + + TrackIds tracks() const; + bool canEditTracks() const; + + public slots: + TrackMetadata GetTracksMetadata(const TrackIds& tracks) const; + void AddTrack(const QString& uri, const QDBusObjectPath& afterTrack, bool setAsCurrent); + void RemoveTrack(const QDBusObjectPath& trackId); + void GoTo(const QDBusObjectPath& trackId); + + signals: + void TrackListReplaced(const TrackIds& tracks, const QDBusObjectPath& currentTrack); + void TrackAdded(const TrackMetadata& metadata, const QDBusObjectPath& afterTrack); + void TrackRemoved(const QDBusObjectPath& trackId); + void TrackMetadataChanged(const QDBusObjectPath& trackId, const TrackMetadata& metadata); +}; + +#endif // MEDIAPLAYER2TRACKLIST_H diff --git a/src/broadcast/mpris/mpris.cpp b/src/broadcast/mpris/mpris.cpp new file mode 100644 index 00000000000..897b9524476 --- /dev/null +++ b/src/broadcast/mpris/mpris.cpp @@ -0,0 +1,58 @@ + +#include +#include + +#include "broadcast/mpris/mediaplayer2.h" +#include "broadcast/mpris/mediaplayer2player.h" +#include "broadcast/mpris/mediaplayer2tracklist.h" +#include "broadcast/mpris/mediaplayer2playlists.h" + +#include "mpris.h" + + +namespace { + const QString busName = "org.mpris.MediaPlayer2.mixxx"; +} + +Mpris::Mpris(MixxxMainWindow *pMixxx, + PlayerManager *pPlayerManager, + UserSettingsPointer pSettings) + : m_busConnection(QDBusConnection::connectToBus(QDBusConnection::SessionBus, busName)), + m_pPlayer(new MediaPlayer2Player(pPlayerManager, + this, pMixxx, + this, pSettings)) { + // Classes derived from QDBusAbstractAdaptor must be created + // on the heap using the new operator and must not be deleted + // by the user (they will be deleted automatically when the object + // they are connected to is also deleted). + // http://doc.qt.io/qt-5/qdbusabstractadaptor.html + new MediaPlayer2(pMixxx, this); + m_busConnection.registerObject("/org/mpris/MediaPlayer2", this); + m_busConnection.registerService("org.mpris.MediaPlayer2.mixxx"); +} + +Mpris::~Mpris() { + m_busConnection.unregisterObject("/org/mpris/MediaPlayer2"); + m_busConnection.unregisterService("org.mpris.MediaPlayer2.mixxx"); + QDBusConnection::disconnectFromBus(busName); +} + +void Mpris::broadcastCurrentTrack() { + notifyPropertyChanged("org.mpris.MediaPlayer2.Player", + "Metadata", m_pPlayer->metadata()); +} + +void Mpris::notifyPropertyChanged(const QString& interface, + const QString& propertyName, + const QVariant& propertyValue) { + QDBusMessage signal = QDBusMessage::createSignal( + "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties", + "PropertiesChanged"); + signal << interface; + QVariantMap changedProps; + changedProps.insert(propertyName, propertyValue); + signal << changedProps; + signal << QStringList(); + m_busConnection.send(signal); +} diff --git a/src/broadcast/mpris/mpris.h b/src/broadcast/mpris/mpris.h new file mode 100644 index 00000000000..5da7f0f38b3 --- /dev/null +++ b/src/broadcast/mpris/mpris.h @@ -0,0 +1,30 @@ +#ifndef MPRIS_H +#define MPRIS_H + +#include +#include +#include "track/track.h" + +class MediaPlayer2Player; +class MixxxMainWindow; +class PlayerManager; + +class Mpris : public QObject +{ + Q_OBJECT + public: + explicit Mpris(MixxxMainWindow *mixxx, + PlayerManager *pPlayerManager, + UserSettingsPointer pSettings); + ~Mpris(); + void broadcastCurrentTrack(); + void notifyPropertyChanged(const QString& interface, + const QString& propertyName, + const QVariant& propertyValue); + private: + + QDBusConnection m_busConnection; + MediaPlayer2Player *m_pPlayer; +}; + +#endif // MPRIS_H diff --git a/src/broadcast/mpris/mprisplayer.cpp b/src/broadcast/mpris/mprisplayer.cpp new file mode 100644 index 00000000000..41fa4ba3069 --- /dev/null +++ b/src/broadcast/mpris/mprisplayer.cpp @@ -0,0 +1,480 @@ + +#include +#include + +#include "broadcast/mpris/mprisplayer.h" +#include "library/coverartcache.h" +#include "mixer/deck.h" +#include "mixer/playermanager.h" +#include "mixer/playerinfo.h" +#include "mprisplayer.h" + + +namespace { + + const QString kPlaybackStatusPlaying = "Playing"; + const QString kPlaybackStatusPaused = "Paused"; + const QString kPlaybackStatusStopped = "Stopped"; + + // the playback will stop when there are no more tracks to play + const QString kLoopStatusNone = "None"; + // The current track will start again from the begining once it has finished playing + const QString kLoopStatusTrack = "Track"; + // The playback loops through a list of tracks + const QString kLoopStatusPlaylist = "Playlist"; + const QString playerInterfaceName = "org.mpris.MediaPlayer2.Player"; +} + +#define AUTODJENABLED m_bComponentsInitialized && m_CPAutoDjEnabled.toBool() +#define AUTODJIDLE AUTODJENABLED && m_CPAutoDJIdle.toBool() + +MprisPlayer::MprisPlayer(PlayerManager *pPlayerManager, + MixxxMainWindow *pWindow, + Mpris *pMpris, + UserSettingsPointer pSettings) + : m_pPlayerManager(pPlayerManager), + m_pWindow(pWindow), + m_bComponentsInitialized(false), + m_bPropertiesEnabled(false), + m_pMpris(pMpris), + m_pSettings(pSettings) { + connect(m_pWindow, &MixxxMainWindow::componentsInitialized, + this, &MprisPlayer::mixxxComponentsInitialized); + QMetaObject::Connection connection = + connect(CoverArtCache::createInstance(),&CoverArtCache::coverFound, + this,&MprisPlayer::slotCoverArtFound); + if (!connection) { + qWarning() << "Couldn't connect CoverFound"; + } +} + +QString MprisPlayer::playbackStatus() const { + if (!AUTODJENABLED) + return kPlaybackStatusStopped; + for (DeckAttributes *attrib : m_deckAttributes) { + if (attrib->isPlaying()) + return kPlaybackStatusPlaying; + } + return kPlaybackStatusPaused; +} + +QString MprisPlayer::loopStatus() const { + if (!AUTODJENABLED) + return kLoopStatusNone; + for (DeckAttributes *attrib : m_deckAttributes) { + if (attrib->isRepeat() && attrib->isPlaying()) + return kLoopStatusTrack; + } + return m_pSettings->getValue(ConfigKey("[Auto DJ]", "Requeue"), false) ? + kLoopStatusPlaylist : kLoopStatusNone; + +} + +void MprisPlayer::setLoopStatus(const QString& value) { + if (value == kLoopStatusNone || value == kLoopStatusTrack) { + for (DeckAttributes *attribute : m_deckAttributes) { + attribute->setRepeat(value == kLoopStatusTrack); + } + m_pSettings->setValue(ConfigKey("[Auto DJ]", "Requeue"), false); + } + else { + for (DeckAttributes *attribute : m_deckAttributes) { + attribute->setRepeat(false); + } + m_pSettings->setValue(ConfigKey("[Auto DJ]", "Requeue"), true); + } +} + +QVariantMap MprisPlayer::metadata() { + TrackPointer pTrack = PlayerInfo::instance().getCurrentPlayingTrack(); + requestMetadataFromTrack(pTrack, false); + return getVariantMapMetadata(); +} + +double MprisPlayer::volume() const { + return getAverageVolume(); +} + +void MprisPlayer::setVolume(double value) { + for (DeckAttributes *attrib : m_deckAttributes) { + ControlProxy volume(ConfigKey(attrib->group, "volume")); + volume.set(value); + } +} + +qlonglong MprisPlayer::position() const { + if (AUTODJIDLE) { + for (unsigned int i = 0; i < m_pPlayerManager->numberOfDecks(); ++i) { + ControlProxy playing(ConfigKey(PlayerManager::groupForDeck(i), "play")); + if (playing.toBool()) { + DeckAttributes *pDeck = m_deckAttributes.at(i); + qlonglong playPosition = + static_cast(pDeck->playPosition() * //Fraction of duration + pDeck->getLoadedTrack()->getDuration() * //Duration in seconds + 1e6); + return playPosition; + } + } + } + return 0; +} + +bool MprisPlayer::canGoNext() const { + return AUTODJIDLE; +} + +bool MprisPlayer::canGoPrevious() const { + return false; +} + +bool MprisPlayer::canPlay() const { + return true; +} + +bool MprisPlayer::canPause() const { + return true; +} + +bool MprisPlayer::canSeek() const { + return AUTODJIDLE; +} + +void MprisPlayer::nextTrack() { + if (AUTODJIDLE) { + m_CPFadeNow.set(true); + } +} + +void MprisPlayer::pause() { + if (AUTODJIDLE) { + DeckAttributes *playingDeck = findPlayingDeck(); + if (playingDeck != nullptr) { + playingDeck->stop(); + m_pMpris->notifyPropertyChanged(playerInterfaceName, "Metadata", QVariantMap()); + m_pausedDeck = playingDeck->group; + } + } + else { + for (DeckAttributes *attribute : m_deckAttributes) { + attribute->stop(); + } + } +} + +void MprisPlayer::playPause() { + if (AUTODJIDLE) { + DeckAttributes *playingDeck = findPlayingDeck(); + if (playingDeck != nullptr) { + playingDeck->stop(); + m_pMpris->notifyPropertyChanged(playerInterfaceName, "Metadata", QVariantMap()); + m_pausedDeck = playingDeck->group; + } + else { + ControlProxy playing(ConfigKey(m_pausedDeck, "play")); + BaseTrackPlayer *player = m_pPlayerManager->getPlayer(m_pausedDeck); + DEBUG_ASSERT(player); + TrackPointer pTrack = player->getLoadedTrack(); + playing.set(true); + } + } + +} + +void MprisPlayer::play() { + if (!m_bComponentsInitialized) { + return; + } + if (!m_CPAutoDjEnabled.toBool()) { + m_CPAutoDjEnabled.set(true); + return; + } + DeckAttributes *playingDeck = findPlayingDeck(); + if (playingDeck == nullptr) { + ControlProxy playing(ConfigKey(m_pausedDeck, "play")); + BaseTrackPlayer *player = m_pPlayerManager->getPlayer(m_pausedDeck); + DEBUG_ASSERT(player); + TrackPointer pTrack = player->getLoadedTrack(); + playing.set(true); + } +} + +qlonglong MprisPlayer::seek(qlonglong offset, bool& success) { + if (AUTODJIDLE) { + DeckAttributes *playingDeck = findPlayingDeck(); + VERIFY_OR_DEBUG_ASSERT(playingDeck) { + success = false; + return 0; + } + double durationSeconds = playingDeck->getLoadedTrack()->getDuration(); + double newPosition = playingDeck->playPosition() + offset / (durationSeconds * 1e6); + if (newPosition < 0.0) { + success = true; + newPosition = 0.0; + playingDeck->setPlayPosition(newPosition); + return 0; + } + if (newPosition > 1.0) { + success = true; + nextTrack(); + return 0; + } + playingDeck->setPlayPosition(newPosition); + success = true; + return static_cast(durationSeconds*1e6) + offset; + } + success = false; + return 0; +} + +qlonglong MprisPlayer::setPosition(const QDBusObjectPath& trackId, qlonglong position, bool& success) { + if (AUTODJIDLE) { + DeckAttributes *playingDeck = findPlayingDeck(); + VERIFY_OR_DEBUG_ASSERT(playingDeck) { + success = false; + return 0; + } + QString path = trackId.path(); + int lastSlashIndex = path.lastIndexOf('/'); + VERIFY_OR_DEBUG_ASSERT(lastSlashIndex != -1) { + success = false; + return 0; + } + int id = path.right(path.size()-lastSlashIndex-1).toInt(); + if (id != playingDeck->getLoadedTrack()->getId().value()) { + success = false; + return 0; + } + double newPosition = position / (playingDeck->getLoadedTrack()->getDuration() * 1e6); + if (newPosition < 0.0 || newPosition > 1.0) { + success = false; + return 0; + } + playingDeck->setPlayPosition(newPosition); + success = true; + return position; + } + success = false; + return 0; +} + +void MprisPlayer::openUri(const QString& uri) { + qDebug() << "openUri" << uri << "not yet implemented"; +} + +void MprisPlayer::mixxxComponentsInitialized() { + m_bComponentsInitialized = true; + + m_CPAutoDjEnabled.initialize(ConfigKey("[AutoDJ]", "enabled")); + m_CPFadeNow.initialize(ConfigKey("[AutoDJ]", "fade_now")); + m_CPAutoDJIdle.initialize(ConfigKey("[AutoDJ]", "idle")); + + for (unsigned int i = 1; i <= m_pPlayerManager->numberOfDecks(); ++i) { + DeckAttributes *attributes = new DeckAttributes + (i, + m_pPlayerManager->getDeck(i), + i%2==0 ? EngineChannel::RIGHT : EngineChannel::LEFT); + m_deckAttributes.append(attributes); + connect(attributes, &DeckAttributes::playChanged, + this, &MprisPlayer::slotPlayChanged); + connect(attributes, &DeckAttributes::playPositionChanged, + this, &MprisPlayer::slotPlayPositionChanged); + ControlProxy *volume = new ControlProxy(ConfigKey(attributes->group, "volume")); + m_CPDeckVolumes.append(volume); + volume->connectValueChanged(this, SLOT(slotVolumeChanged(double))); + } + + m_CPAutoDjEnabled.connectValueChanged(this, SLOT(slotChangeProperties(double))); + m_CPAutoDJIdle.connectValueChanged(this, SLOT(slotChangeProperties(double))); +} + +void MprisPlayer::slotChangeProperties(double enabled) { + if (enabled != m_bPropertiesEnabled) { + broadcastPropertiesChange(enabled); + m_bPropertiesEnabled = static_cast(enabled); + } +} + +void MprisPlayer::broadcastPropertiesChange(bool enabled) { + for (const QString& property : autoDJDependentProperties) { + m_pMpris->notifyPropertyChanged(playerInterfaceName, + property, + enabled); + } +} + +void MprisPlayer::requestMetadataFromTrack(TrackPointer pTrack, bool requestCover) { + if (!pTrack) + return; + m_currentMetadata.trackPath = + "/org/mixxx/" + pTrack->getId().toString(); + double trackDurationSeconds = pTrack->getDuration(); + trackDurationSeconds *= 1e6; + m_currentMetadata.trackDuration = + static_cast(trackDurationSeconds); + QStringList artists; + artists << pTrack->getArtist(); + m_currentMetadata.artists = artists; + m_currentMetadata.title = pTrack->getTitle(); + if (requestCover) { + requestCoverartUrl(pTrack); + } +} + +void MprisPlayer::requestCoverartUrl(TrackPointer pTrack) { + CoverInfo coverInfo = pTrack->getCoverInfoWithLocation(); + if (coverInfo.type == CoverInfoRelative::FILE) { + QFileInfo fileInfo; + if (!coverInfo.trackLocation.isEmpty()) { + fileInfo = QFileInfo(coverInfo.trackLocation); + } + QFileInfo coverFile(fileInfo.dir(), coverInfo.coverLocation); + QString path = coverFile.absoluteFilePath(); + qDebug() << "Cover art path: " << path; + QUrl fileUrl = QUrl::fromLocalFile(path); + if (!fileUrl.isValid()) { + qDebug() << "Invalid URL: " << fileUrl; + return; + } + m_currentMetadata.coverartUrl = fileUrl.toString(); + broadcastCurrentMetadata(); + } else if (coverInfo.type == CoverInfoRelative::METADATA) { + CoverArtCache *cache = CoverArtCache::instance(); + QPixmap coverPixMap = cache->requestCover(coverInfo, + this, + 0, + false, + true); + } + else { + m_currentMetadata.coverartUrl.clear(); + broadcastCurrentMetadata(); + } +} + +void MprisPlayer::slotPlayChanged(DeckAttributes *pDeck, bool playing) { + if (!AUTODJENABLED) + return; + bool otherDeckPlaying = false; + DeckAttributes *playingDeck = playing ? pDeck : nullptr; + for (int i = 0; i < m_deckAttributes.size(); ++i) { + if (m_deckAttributes.at(i)->group != pDeck->group && + m_deckAttributes.at(i)->isPlaying()) { + otherDeckPlaying = true; + playingDeck = m_deckAttributes.at(i); + break; + } + } + m_pMpris->notifyPropertyChanged(playerInterfaceName, "PlaybackStatus", + playing || otherDeckPlaying ? + kPlaybackStatusPlaying : + kPlaybackStatusPaused); + if (!playing && !otherDeckPlaying) { + m_pMpris->notifyPropertyChanged(playerInterfaceName, "Metadata", + QVariantMap()); + } + else if (!playing || !otherDeckPlaying) { + requestMetadataFromTrack(playingDeck->getLoadedTrack(), true); + } +} + +MprisPlayer::~MprisPlayer() { + for (DeckAttributes *attrib : m_deckAttributes) { + delete attrib; + } +} + +void MprisPlayer::slotPlayPositionChanged(DeckAttributes *pDeck, double position) { + if (AUTODJIDLE) { + qlonglong playPosition = static_cast(position * //Fraction of duration + pDeck->getLoadedTrack()->getDuration() * //Duration in seconds + 1e6); + m_pMpris->notifyPropertyChanged(playerInterfaceName, "Position", playPosition); + } +} + +DeckAttributes *MprisPlayer::findPlayingDeck() const { + for (int i = 0; i < m_deckAttributes.count(); ++i) { + if (m_deckAttributes.at(i)->isPlaying()) { + return m_deckAttributes.at(i); + } + } + return nullptr; +} + +void MprisPlayer::slotVolumeChanged(double volume) { + Q_UNUSED(volume); + double averageVolume = getAverageVolume(); + m_pMpris->notifyPropertyChanged(playerInterfaceName, "Volume", averageVolume); +} + +double MprisPlayer::getAverageVolume() const { + double averageVolume = 0.0; + unsigned int numberOfPlayingDecks = 0; + for (DeckAttributes *attrib : m_deckAttributes) { + if (attrib->isPlaying()) { + ControlProxy volume(ConfigKey(attrib->group, "volume")); + averageVolume += volume.get(); + ++numberOfPlayingDecks; + } + } + averageVolume /= numberOfPlayingDecks; + return averageVolume; +} + +double MprisPlayer::rate() const { + DeckAttributes *playingDeck = findPlayingDeck(); + if (playingDeck != nullptr) { + ControlProxy rate(ConfigKey( + PlayerManager::groupForDeck(playingDeck->index-1), "rate")); + return rate.get(); + } + return 0; +} + +void MprisPlayer::setRate(double value) { + DeckAttributes *playingDeck = findPlayingDeck(); + if (playingDeck != nullptr) { + ControlProxy rate(ConfigKey( + PlayerManager::groupForDeck(playingDeck->index-1), "rate")); + double clampedValue = qBound(-1.0, value, 1.0); + rate.set(clampedValue); + } +} + +void MprisPlayer::slotCoverArtFound(const QObject *requestor, + const CoverInfoRelative &info, + QPixmap pixmap, + bool fromCache) { + Q_UNUSED(info); + Q_UNUSED(fromCache); + + if (!pixmap.isNull() && requestor == this) { + QImage coverImage = pixmap.toImage(); + m_currentCoverArtFile.open(); + bool success = coverImage.save(&m_currentCoverArtFile,"JPG"); + if (!success) { + qDebug() << "Couldn't write metadata cover art"; + return; + } + m_currentCoverArtFile.close(); + QUrl fileUrl = QUrl::fromLocalFile(m_currentCoverArtFile.fileName()); + m_currentMetadata.coverartUrl = fileUrl.toString(); + broadcastCurrentMetadata(); + } + +} + +void MprisPlayer::broadcastCurrentMetadata() { + m_pMpris->notifyPropertyChanged(playerInterfaceName, "Metadata", + getVariantMapMetadata()); +} + +QVariantMap MprisPlayer::getVariantMapMetadata() { + QVariantMap metadata; + metadata.insert("mpris:trackid", m_currentMetadata.trackPath); + metadata.insert("mpris:length", m_currentMetadata.trackDuration); + metadata.insert("xesam:artist", m_currentMetadata.artists); + metadata.insert("xesam:title", m_currentMetadata.title); + metadata.insert("mpris:artUrl",m_currentMetadata.coverartUrl); + return metadata; +} diff --git a/src/broadcast/mpris/mprisplayer.h b/src/broadcast/mpris/mprisplayer.h new file mode 100644 index 00000000000..e4e6d4bfd4b --- /dev/null +++ b/src/broadcast/mpris/mprisplayer.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include + +#include "broadcast/mpris/mpris.h" +#include "control/controlproxy.h" +#include "library/autodj/autodjprocessor.h" + +class PlayerManager; + +class MprisPlayer : public QObject { + Q_OBJECT + public: + MprisPlayer(PlayerManager *pPlayerManager, + MixxxMainWindow *pWindow, + Mpris *pMpris, + UserSettingsPointer pSettings); + + ~MprisPlayer() override; + QString playbackStatus() const; + QString loopStatus() const; + void setLoopStatus(const QString& value); + double rate() const; + void setRate(double value); + QVariantMap metadata(); + double volume() const; + void setVolume(double value); + qlonglong position() const; + bool canGoNext() const; + bool canGoPrevious() const; + bool canPlay() const; + bool canPause() const; + bool canSeek() const; + void nextTrack(); + void pause(); + void playPause(); + void play(); + qlonglong seek(qlonglong offset, bool& success); + qlonglong setPosition(const QDBusObjectPath& trackId, qlonglong position, bool& success); + void openUri(const QString& uri); + + private slots: + void mixxxComponentsInitialized(); + void slotChangeProperties(double enabled); + void slotPlayChanged(DeckAttributes *pDeck, bool playing); + void slotPlayPositionChanged(DeckAttributes *pDeck, double position); + void slotVolumeChanged(double volume); + void slotCoverArtFound(const QObject *requestor, + const CoverInfoRelative& info, + QPixmap pixmap, + bool fromCache); + + private: + + void broadcastPropertiesChange(bool enabled); + void requestMetadataFromTrack(TrackPointer pTrack, bool requestCover); + void requestCoverartUrl(TrackPointer pTrack); + void broadcastCurrentMetadata(); + QVariantMap getVariantMapMetadata(); + double getAverageVolume() const; + DeckAttributes* findPlayingDeck() const; + const QString autoDJDependentProperties[4] = { + "CanGoNext", + "CanPlay", + "CanPause", + "CanSeek" + }; + + ControlProxy m_CPAutoDjEnabled; + ControlProxy m_CPFadeNow; + ControlProxy m_CPAutoDJIdle; + QList m_CPDeckVolumes; + PlayerManager *m_pPlayerManager; + MixxxMainWindow *m_pWindow; + QString m_pausedDeck; + bool m_bComponentsInitialized, m_bPropertiesEnabled; + Mpris *m_pMpris; + QList m_deckAttributes; + UserSettingsPointer m_pSettings; + + struct CurrentMetadata { + QString trackPath; + long long int trackDuration; + QStringList artists; + QString title; + QString coverartUrl; + }; + + CurrentMetadata m_currentMetadata; + QTemporaryFile m_currentCoverArtFile; +}; diff --git a/src/broadcast/mpris/mprisservice.cpp b/src/broadcast/mpris/mprisservice.cpp new file mode 100644 index 00000000000..2fb09bcf022 --- /dev/null +++ b/src/broadcast/mpris/mprisservice.cpp @@ -0,0 +1,29 @@ + +#include "broadcast/mpris/mprisservice.h" + +MprisService::MprisService(MixxxMainWindow *pWindow, + PlayerManager *pPlayer, + UserSettingsPointer pSettings) + : m_mpris(pWindow, pPlayer, pSettings) { + connect(pWindow, &MixxxMainWindow::componentsInitialized, + this, &MprisService::slotComponentsInitialized); +} + +void MprisService::slotBroadcastCurrentTrack(TrackPointer pTrack) { + if (!m_CPAutoDJEnabled.toBool()) { + m_mpris.broadcastCurrentTrack(); + } +} + +void MprisService::slotScrobbleTrack(TrackPointer pTrack) { + Q_UNUSED(pTrack); +} + +void MprisService::slotAllTracksPaused() { +} + +void MprisService::slotComponentsInitialized() { + m_CPAutoDJEnabled.initialize(ConfigKey("[AutoDJ]", "enabled")); +} + + diff --git a/src/broadcast/mpris/mprisservice.h b/src/broadcast/mpris/mprisservice.h new file mode 100644 index 00000000000..ca88968b947 --- /dev/null +++ b/src/broadcast/mpris/mprisservice.h @@ -0,0 +1,22 @@ +#pragma once + + +#include "broadcast/scrobblingservice.h" +#include "broadcast/mpris/mpris.h" +#include "mixxx.h" + +class MprisService : public ScrobblingService { + Q_OBJECT + public: + explicit MprisService(MixxxMainWindow *pWindow, + PlayerManager *pPlayer, + UserSettingsPointer pSettings); + void slotBroadcastCurrentTrack(TrackPointer pTrack) override; + void slotScrobbleTrack(TrackPointer pTrack) override; + void slotAllTracksPaused() override; + private slots: + void slotComponentsInitialized(); + private: + Mpris m_mpris; + ControlProxy m_CPAutoDJEnabled; +}; diff --git a/src/broadcast/scrobblingmanager.cpp b/src/broadcast/scrobblingmanager.cpp new file mode 100644 index 00000000000..297f2513a7e --- /dev/null +++ b/src/broadcast/scrobblingmanager.cpp @@ -0,0 +1,210 @@ +#include + +#include "broadcast/filelistener/filelistener.h" +#include "broadcast/scrobblingmanager.h" +#include "control/controlproxy.h" +#include "engine/enginexfader.h" +#include "mixer/deck.h" +#include "mixer/playerinfo.h" +#include "mixer/playermanager.h" + +#ifdef MIXXX_BUILD_DEBUG +QDebug operator<<(QDebug debug, const ScrobblingManager::TrackInfo& info) { + debug << "Pointer to track:" << info.m_trackInfo.get(); + return debug << "Players:" << info.m_players; +} +#endif + +TotalVolumeThreshold::TotalVolumeThreshold(QObject *parent, double threshold) + : m_CPCrossfader("[Master]", "crossfader", parent), + m_CPXFaderCurve(ConfigKey(EngineXfader::kXfaderConfigKey, + "xFaderCurve"), parent), + m_CPXFaderCalibration(ConfigKey(EngineXfader::kXfaderConfigKey, + "xFaderCalibration"), parent), + m_CPXFaderMode(ConfigKey(EngineXfader::kXfaderConfigKey, + "xFaderMode"), parent), + m_CPXFaderReverse(ConfigKey(EngineXfader::kXfaderConfigKey, + "xFaderReverse"), parent), + m_pParent(parent), + m_volumeThreshold(threshold) { + +} + +bool TotalVolumeThreshold::isPlayerAudible(BaseTrackPlayer *pPlayer) const { + DEBUG_ASSERT(pPlayer); + double finalVolume; + ControlProxy trackPreGain(pPlayer->getGroup(), "pregain", m_pParent); + double preGain = trackPreGain.get(); + ControlProxy trackVolume(pPlayer->getGroup(), "volume", m_pParent); + double volume = trackVolume.get(); + ControlProxy deckOrientation(pPlayer->getGroup(), "orientation", m_pParent); + int orientation = deckOrientation.get(); + + double xFaderLeft, xFaderRight; + + EngineXfader::getXfadeGains(m_CPCrossfader.get(), + m_CPXFaderCurve.get(), + m_CPXFaderCalibration.get(), + m_CPXFaderMode.get(), + m_CPXFaderReverse.toBool(), + &xFaderLeft, &xFaderRight); + finalVolume = preGain * volume; + if (orientation == EngineChannel::LEFT) + finalVolume *= xFaderLeft; + else if (orientation == EngineChannel::RIGHT) + finalVolume *= xFaderRight; + return finalVolume > m_volumeThreshold; +} + +void TotalVolumeThreshold::setVolumeThreshold(double volume) { + m_volumeThreshold = volume; +} + +ScrobblingManager::ScrobblingManager(PlayerManagerInterface *manager) + : m_pManager(manager), + m_pAudibleStrategy(new TotalVolumeThreshold(this, 0.20)), + m_pTimer(new TrackTimers::GUITickTimer), + m_scrobbledAtLeastOnce(false), + m_GuiTickObject(ConfigKey("[Master]", "guiTick50ms")){ + connect(m_pTimer.get(), &TrackTimers::RegularTimer::timeout, + this, &ScrobblingManager::slotCheckAudibleTracks); + m_GuiTickObject.connectValueChanged(this, SLOT(slotGuiTick(double))); + m_pTimer->start(1000); +} + +void ScrobblingManager::setAudibleStrategy(TrackAudibleStrategy *pStrategy) { + m_pAudibleStrategy.reset(pStrategy); +} + +void ScrobblingManager::setMetadataBroadcaster(MetadataBroadcasterInterface *pBroadcast) { + m_pBroadcaster.reset(pBroadcast); + connect(&PlayerInfo::instance(), SIGNAL(currentPlayingTrackChanged(TrackPointer)), + m_pBroadcaster.get(), SLOT(slotNowListening(TrackPointer))); +} + +void ScrobblingManager::setTimer(TrackTimers::RegularTimer *timer) { + m_pTimer.reset(timer); +} + +void ScrobblingManager::setTrackInfoFactory( + const std::function(TrackPointer)>& factory) { + m_trackInfoFactory = factory; +} + +bool ScrobblingManager::hasScrobbledAnyTrack() const { + return m_scrobbledAtLeastOnce; +} + + +void ScrobblingManager::slotTrackPaused(TrackPointer pPausedTrack) { + if (!pPausedTrack) + return; + if (!m_trackInfoHashDict.contains(pPausedTrack->getId())) { + m_trackInfoHashDict[pPausedTrack->getId()].init(m_trackInfoFactory, pPausedTrack); + } +#ifdef MIXXX_BUILD_DEBUG + qDebug() << "Hash:" << m_trackInfoHashDict; +#endif + DEBUG_ASSERT(m_trackInfoHashDict.contains(pPausedTrack->getId())); + for (QString playerString : m_trackInfoHashDict.value(pPausedTrack->getId()).m_players) { + BaseTrackPlayer *player = m_pManager->getPlayer(playerString); + DEBUG_ASSERT(player); + if (!player->isTrackPaused()) { + return; + } + } + m_trackInfoHashDict[pPausedTrack->getId()].m_trackInfo->pausePlayedTime(); +} + +void ScrobblingManager::slotTrackResumed(TrackPointer pResumedTrack, const QString& playerGroup) { + if (!pResumedTrack) + return; + BaseTrackPlayer *player = m_pManager->getPlayer(playerGroup); + if (!m_trackInfoHashDict.contains(pResumedTrack->getId())) { + m_trackInfoHashDict[pResumedTrack->getId()].init(m_trackInfoFactory, pResumedTrack); + } +#ifdef MIXXX_BUILD_DEBUG + qDebug() << "Hash:" << m_trackInfoHashDict; +#endif + DEBUG_ASSERT(player && m_trackInfoHashDict.contains(pResumedTrack->getId())); + if (m_pAudibleStrategy->isPlayerAudible(player)) { + TrackInfo info = m_trackInfoHashDict.value(pResumedTrack->getId()); + if (info.m_trackInfo->isTimerPaused()) { + info.m_trackInfo->resumePlayedTime(); + } + } +} + +void ScrobblingManager::slotNewTrackLoaded(TrackPointer pNewTrack, const QString& playerGroup) { + //Empty player gives a null pointer. + if (!pNewTrack) { + return; + } + if (!m_trackInfoHashDict.contains(pNewTrack->getId())) { + m_trackInfoHashDict[pNewTrack->getId()].init(m_trackInfoFactory, pNewTrack); + } + m_trackInfoHashDict[pNewTrack->getId()].m_players.insert(playerGroup); + connect(m_trackInfoHashDict[pNewTrack->getId()].m_trackInfo.get(), + &TrackTimingInfo::readyToBeScrobbled, + this, &ScrobblingManager::slotReadyToBeScrobbled); + m_pBroadcaster->newTrackLoaded(pNewTrack); + auto it = m_trackInfoHashDict.begin(); + while (it != m_trackInfoHashDict.end()) { + if (it->m_players.contains(playerGroup) && it.key() != pNewTrack->getId()) { + it->m_players.remove(playerGroup); + if (it->m_players.empty()) { + it = m_trackInfoHashDict.erase(it); + } + else { + ++it; + } + } + else { + ++it; + } + } + + +} + + +void ScrobblingManager::slotGuiTick(double timeSinceLastTick) { + for (auto& trackInfo : m_trackInfoHashDict) { + trackInfo.m_trackInfo->slotGuiTick(timeSinceLastTick); + } + + MetadataBroadcaster *broadcaster = + qobject_cast(m_pBroadcaster.get()); + if (broadcaster) + broadcaster->guiTick(timeSinceLastTick); + + TrackTimers::GUITickTimer *timer = + qobject_cast(m_pTimer.get()); + if (timer) + timer->slotTick(timeSinceLastTick); +} + +void ScrobblingManager::slotReadyToBeScrobbled(TrackPointer pTrack) { + m_scrobbledAtLeastOnce = true; + m_pBroadcaster->slotAttemptScrobble(pTrack); +} + +void ScrobblingManager::slotCheckAudibleTracks() { + for (auto& trackInfo : m_trackInfoHashDict) { + bool audible = false; + for (QString playerGroup : trackInfo.m_players) { + BaseTrackPlayer *player = m_pManager->getPlayer(playerGroup); + if (m_pAudibleStrategy->isPlayerAudible(player)) { + audible = true; + break; + } + } + if (!audible) { + trackInfo.m_trackInfo->pausePlayedTime(); + } + else if (trackInfo.m_trackInfo->isTimerPaused()){ + trackInfo.m_trackInfo->resumePlayedTime(); + } + } + m_pTimer->start(1000); +} \ No newline at end of file diff --git a/src/broadcast/scrobblingmanager.h b/src/broadcast/scrobblingmanager.h new file mode 100644 index 00000000000..644749a8d22 --- /dev/null +++ b/src/broadcast/scrobblingmanager.h @@ -0,0 +1,106 @@ +#include + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "control/controlobject.h" +#include "broadcast/metadatabroadcast.h" +#include "mixer/basetrackplayer.h" +#include "track/track.h" +#include "track/tracktiminginfo.h" +#include "track/trackplaytimers.h" + +class BaseTrackPlayer; +class PlayerManager; +class PlayerManagerInterface; +class MixxxMainWindow; + +class TrackAudibleStrategy { + public: + virtual ~TrackAudibleStrategy() = default; + virtual bool isPlayerAudible(BaseTrackPlayer *pPlayer) const = 0; +}; + +class TotalVolumeThreshold : public TrackAudibleStrategy { + public: + TotalVolumeThreshold(QObject *parent, double threshold); + bool isPlayerAudible(BaseTrackPlayer *pPlayer) const override; + void setVolumeThreshold(double volume); + private: + ControlProxy m_CPCrossfader; + ControlProxy m_CPXFaderCurve; + ControlProxy m_CPXFaderCalibration; + ControlProxy m_CPXFaderMode; + ControlProxy m_CPXFaderReverse; + + QObject *m_pParent; + + double m_volumeThreshold; +}; + +typedef std::function(TrackPointer)> TrackTimingFactory; + +class ScrobblingManager : public QObject { + Q_OBJECT + public: + explicit ScrobblingManager(PlayerManagerInterface *manager); + ~ScrobblingManager() = default; + void setAudibleStrategy(TrackAudibleStrategy *pStrategy); + void setMetadataBroadcaster(MetadataBroadcasterInterface *pBroadcast); + void setTimer(TrackTimers::RegularTimer *timer); + void setTrackInfoFactory(const std::function(TrackPointer)>& factory); + bool hasScrobbledAnyTrack() const; + + public slots: + void slotTrackPaused(TrackPointer pPausedTrack); + void slotTrackResumed(TrackPointer pResumedTrack, const QString& playerGroup); + void slotNewTrackLoaded(TrackPointer pNewTrack, const QString& playerGroup); + + private: + + struct TrackInfo { + std::shared_ptr m_trackInfo; + QSet m_players; + void init (const TrackTimingFactory& factory, + const TrackPointer& pTrack) { + if (factory) { + m_trackInfo = factory(pTrack); + } + else { + m_trackInfo = std::make_shared(pTrack); + } + } + }; + +#ifdef MIXXX_BUILD_DEBUG + friend QDebug operator<<(QDebug debug, const ScrobblingManager::TrackInfo& info); +#endif + + QHash m_trackInfoHashDict; + + PlayerManagerInterface *m_pManager; + + std::unique_ptr m_pBroadcaster; + + + std::unique_ptr m_pAudibleStrategy; + + std::unique_ptr m_pTimer; + + TrackTimingFactory m_trackInfoFactory; + + bool m_scrobbledAtLeastOnce; + + ControlProxy m_GuiTickObject; + +private slots: + void slotReadyToBeScrobbled(TrackPointer pTrack); + void slotCheckAudibleTracks(); + void slotGuiTick(double timeSinceLastTick); +}; \ No newline at end of file diff --git a/src/broadcast/scrobblingservice.h b/src/broadcast/scrobblingservice.h new file mode 100644 index 00000000000..7d1eceeae28 --- /dev/null +++ b/src/broadcast/scrobblingservice.h @@ -0,0 +1,14 @@ +#pragma once + +#include "track/track.h" + +class ScrobblingService : public QObject { + public: + ~ScrobblingService() override = default; + public slots: + virtual void slotBroadcastCurrentTrack(TrackPointer pTrack) = 0; + virtual void slotScrobbleTrack(TrackPointer pTrack) = 0; + virtual void slotAllTracksPaused() = 0; +}; + +typedef std::shared_ptr ScrobblingServicePtr; \ No newline at end of file diff --git a/src/library/autodj/autodjprocessor.cpp b/src/library/autodj/autodjprocessor.cpp index e9b3c642dd4..696ea30ae93 100644 --- a/src/library/autodj/autodjprocessor.cpp +++ b/src/library/autodj/autodjprocessor.cpp @@ -100,6 +100,10 @@ AutoDJProcessor::AutoDJProcessor(QObject* pParent, connect(m_pEnabledAutoDJ, SIGNAL(valueChanged(double)), this, SLOT(controlEnable(double))); + m_pIdleState = new ControlPushButton( + ConfigKey("[AutoDJ]","idle")); + + // TODO(rryan) listen to signals from PlayerManager and add/remove as decks // are created. for (unsigned int i = 0; i < pPlayerManager->numberOfDecks(); ++i) { @@ -138,6 +142,7 @@ AutoDJProcessor::~AutoDJProcessor() { delete m_pShufflePlaylist; delete m_pEnabledAutoDJ; delete m_pFadeNow; + delete m_pIdleState; delete m_pAutoDJTableModel; } @@ -332,7 +337,7 @@ AutoDJProcessor::AutoDJError AutoDJProcessor::toggleAutoDJ(bool enable) { // loaded track from the queue and wait for the next call to // playerPositionChanged for deck1 after the track is loaded. m_eState = ADJ_ENABLE_P1LOADED; - + m_pIdleState->set(false); // Move crossfader to the left. setCrossfader(-1.0, false); @@ -344,6 +349,7 @@ AutoDJProcessor::AutoDJError AutoDJProcessor::toggleAutoDJ(bool enable) { // One of the two decks is playing. Switch into IDLE mode and wait // until the playing deck crosses posThreshold to start fading. m_eState = ADJ_IDLE; + m_pIdleState->set(true); if (deck1Playing) { // Update fade thresholds for the left deck. calculateTransition(&deck1, &deck2); @@ -367,6 +373,7 @@ AutoDJProcessor::AutoDJError AutoDJProcessor::toggleAutoDJ(bool enable) { } qDebug() << "Auto DJ disabled"; m_eState = ADJ_DISABLED; + m_pIdleState->set(false); deck1.disconnect(this); deck2.disconnect(this); m_pCOCrossfader->set(0); @@ -446,7 +453,7 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes, // sure our thresholds are configured (by calling calculateFadeThresholds // for the playing deck). m_eState = ADJ_IDLE; - + m_pIdleState->set(true); if (!rightDeckPlaying) { // Only left deck playing! // In ADJ_ENABLE_P1LOADED mode we wait until the left deck @@ -489,6 +496,7 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes, setCrossfader(1.0, true); } m_eState = ADJ_IDLE; + m_pIdleState->set(true); // Invalidate threshold calculated for the old otherDeck // This avoids starting a fade back before the new track is // loaded into the otherDeck @@ -550,6 +558,7 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes, // Set the state as FADING. m_eState = thisDeck.isLeft() ? ADJ_P1FADING : ADJ_P2FADING; + m_pIdleState->set(false); emitAutoDJStateChanged(m_eState); } diff --git a/src/library/autodj/autodjprocessor.h b/src/library/autodj/autodjprocessor.h index 8a560a50b9b..261dbfb17b3 100644 --- a/src/library/autodj/autodjprocessor.h +++ b/src/library/autodj/autodjprocessor.h @@ -209,6 +209,7 @@ class AutoDJProcessor : public QObject { ControlPushButton* m_pFadeNow; ControlPushButton* m_pShufflePlaylist; ControlPushButton* m_pEnabledAutoDJ; + ControlPushButton* m_pIdleState; DISALLOW_COPY_AND_ASSIGN(AutoDJProcessor); }; diff --git a/src/library/autodj/dlgautodj.cpp b/src/library/autodj/dlgautodj.cpp index 6798d0a6183..ae3d5a5d041 100644 --- a/src/library/autodj/dlgautodj.cpp +++ b/src/library/autodj/dlgautodj.cpp @@ -180,13 +180,9 @@ void DlgAutoDJ::autoDJStateChanged(AutoDJProcessor::AutoDJState state) { pushButtonAutoDJ->setText(tr("Disable Auto DJ")); // If fading, you can't hit fade now. - if (state == AutoDJProcessor::ADJ_P1FADING || - state == AutoDJProcessor::ADJ_P2FADING || - state == AutoDJProcessor::ADJ_ENABLE_P1LOADED) { - pushButtonFadeNow->setEnabled(false); - } else { - pushButtonFadeNow->setEnabled(true); - } + pushButtonFadeNow->setEnabled(!(state == AutoDJProcessor::ADJ_P1FADING || + state == AutoDJProcessor::ADJ_P2FADING || + state == AutoDJProcessor::ADJ_ENABLE_P1LOADED)); // You can always skip the next track if we are enabled. pushButtonSkipNext->setEnabled(true); diff --git a/src/mixer/basetrackplayer.cpp b/src/mixer/basetrackplayer.cpp index 1a836dcf928..ff16b5ce70c 100644 --- a/src/mixer/basetrackplayer.cpp +++ b/src/mixer/basetrackplayer.cpp @@ -387,6 +387,10 @@ void BaseTrackPlayerImpl::slotSetReplayGain(mixxx::ReplayGain replayGain) { } void BaseTrackPlayerImpl::slotPlayToggled(double v) { + if (v == 0) + emit(trackPaused(m_pLoadedTrack)); + else if (v == 1) + emit(trackResumed(m_pLoadedTrack)); if (!v && m_replaygainPending) { setReplayGain(m_pLoadedTrack->getReplayGain().getRatio()); } @@ -472,3 +476,7 @@ void BaseTrackPlayerImpl::setReplayGain(double value) { m_pReplayGain->set(value); m_replaygainPending = false; } + +bool BaseTrackPlayerImpl::isTrackPaused() const { + return !m_pPlay->toBool(); +} diff --git a/src/mixer/basetrackplayer.h b/src/mixer/basetrackplayer.h index 3133f20eb97..fdf94f3e44b 100644 --- a/src/mixer/basetrackplayer.h +++ b/src/mixer/basetrackplayer.h @@ -35,6 +35,8 @@ class BaseTrackPlayer : public BasePlayer { virtual TrackPointer getLoadedTrack() const = 0; + virtual bool isTrackPaused() const {return false;} + public slots: virtual void slotLoadTrack(TrackPointer pTrack, bool bPlay = false) = 0; @@ -42,6 +44,9 @@ class BaseTrackPlayer : public BasePlayer { void newTrackLoaded(TrackPointer pLoadedTrack); void loadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack); void playerEmpty(); + void trackLoadFailed(TrackPointer pFailedTrack); + void trackPaused(TrackPointer pPausedTrack); + void trackResumed(TrackPointer pResumedTrack); void noPassthroughInputConfigured(); void noVinylControlInputConfigured(); }; @@ -70,6 +75,8 @@ class BaseTrackPlayerImpl : public BaseTrackPlayer { // For testing, loads a fake track. TrackPointer loadFakeTrack(bool bPlay, double filebpm); + bool isTrackPaused() const override; + public slots: void slotLoadTrack(TrackPointer track, bool bPlay) final; void slotTrackLoaded(TrackPointer pNewTrack, TrackPointer pOldTrack); diff --git a/src/mixer/playermanager.cpp b/src/mixer/playermanager.cpp index 97c1aa55be0..cb54acb485d 100644 --- a/src/mixer/playermanager.cpp +++ b/src/mixer/playermanager.cpp @@ -1,10 +1,15 @@ -// playermanager.cpp + // playermanager.cpp // Created 6/1/2010 by RJ Ryan (rryan@mit.edu) #include "mixer/playermanager.h" #include #include "analyzer/analyzerqueue.h" +#include "broadcast/filelistener/filelistener.h" +#include "broadcast/listenbrainzlistener/listenbrainzservice.h" +#ifdef __MPRIS__ +#include "broadcast/mpris/mprisservice.h" +#endif #include "control/controlobject.h" #include "control/controlobject.h" #include "effects/effectsmanager.h" @@ -23,6 +28,7 @@ #include "util/assert.h" #include "util/stat.h" #include "util/sleepableqthread.h" +#include "broadcast/metadatabroadcast.h" //static QAtomicPointer PlayerManager::m_pCOPNumDecks; @@ -31,10 +37,8 @@ QAtomicPointer PlayerManager::m_pCOPNumSamplers; //static QAtomicPointer PlayerManager::m_pCOPNumPreviewDecks; -PlayerManager::PlayerManager(UserSettingsPointer pConfig, - SoundManager* pSoundManager, - EffectsManager* pEffectsManager, - EngineMaster* pEngine) : +PlayerManager::PlayerManager(UserSettingsPointer pConfig, SoundManager *pSoundManager, EffectsManager *pEffectsManager, + EngineMaster *pEngine, MixxxMainWindow *pWindow) : m_mutex(QMutex::Recursive), m_pConfig(pConfig), m_pSoundManager(pSoundManager), @@ -43,6 +47,7 @@ PlayerManager::PlayerManager(UserSettingsPointer pConfig, // NOTE(XXX) LegacySkinParser relies on these controls being Controls // and not ControlProxies. m_pAnalyzerQueue(nullptr), + m_scrobblingManager(this), m_pCONumDecks(new ControlObject( ConfigKey("[Master]", "num_decks"), true, true)), m_pCONumSamplers(new ControlObject( @@ -52,7 +57,8 @@ PlayerManager::PlayerManager(UserSettingsPointer pConfig, m_pCONumMicrophones(new ControlObject( ConfigKey("[Master]", "num_microphones"), true, true)), m_pCONumAuxiliaries(new ControlObject( - ConfigKey("[Master]", "num_auxiliaries"), true, true)) { + ConfigKey("[Master]", "num_auxiliaries"), true, true)) + { connect(m_pCONumDecks, SIGNAL(valueChanged(double)), this, SLOT(slotNumDecksControlChanged(double)), Qt::DirectConnection); @@ -86,6 +92,14 @@ PlayerManager::PlayerManager(UserSettingsPointer pConfig, // This is parented to the PlayerManager so does not need to be deleted m_pSamplerBank = new SamplerBank(this); + + MetadataBroadcaster *broadcaster = new MetadataBroadcaster; + broadcaster->addNewScrobblingService(ScrobblingServicePtr(new FileListener(pConfig))); + broadcaster->addNewScrobblingService(ScrobblingServicePtr(new ListenBrainzService(pConfig))); +#ifdef __MPRIS__ + broadcaster->addNewScrobblingService(ScrobblingServicePtr(new MprisService(pWindow, this, pConfig))); +#endif + m_scrobblingManager.setMetadataBroadcaster(broadcaster); } PlayerManager::~PlayerManager() { @@ -370,6 +384,15 @@ void PlayerManager::addDeckInner() { connect(pDeck, SIGNAL(noVinylControlInputConfigured()), this, SIGNAL(noVinylControlInputConfigured())); + connect(pDeck,&Deck::trackPaused, + &m_scrobblingManager, &ScrobblingManager::slotTrackPaused); + connect(pDeck,&Deck::trackResumed, + [this,group] (TrackPointer pTrack) -> void + {m_scrobblingManager.slotTrackResumed(pTrack,group);}); + connect(pDeck,&Deck::newTrackLoaded, + [this,group] (TrackPointer pTrack) -> void + {m_scrobblingManager.slotNewTrackLoaded(pTrack,group);}); + if (m_pAnalyzerQueue) { connect(pDeck, SIGNAL(newTrackLoaded(TrackPointer)), m_pAnalyzerQueue, SLOT(slotAnalyseTrack(TrackPointer))); @@ -624,3 +647,4 @@ void PlayerManager::slotLoadTrackIntoNextAvailableSampler(TrackPointer pTrack) { ++it; } } + diff --git a/src/mixer/playermanager.h b/src/mixer/playermanager.h index 3c87159ce11..bbb722852d8 100644 --- a/src/mixer/playermanager.h +++ b/src/mixer/playermanager.h @@ -11,6 +11,7 @@ #include "preferences/usersettings.h" #include "track/track.h" +#include "broadcast/scrobblingmanager.h" class AnalyzerQueue; class Auxiliary; @@ -25,6 +26,7 @@ class PreviewDeck; class Sampler; class SamplerBank; class SoundManager; +class MixxxMainWindow; // For mocking PlayerManager. class PlayerManagerInterface { @@ -58,9 +60,11 @@ class PlayerManager : public QObject, public PlayerManagerInterface { Q_OBJECT public: PlayerManager(UserSettingsPointer pConfig, - SoundManager* pSoundManager, - EffectsManager* pEffectsManager, - EngineMaster* pEngine); + SoundManager *pSoundManager, + EffectsManager *pEffectsManager, + EngineMaster *pEngine, + MixxxMainWindow *pWindow); + virtual ~PlayerManager(); // Add a deck to the PlayerManager @@ -233,16 +237,21 @@ class PlayerManager : public QObject, public PlayerManagerInterface { // Must hold m_mutex before calling this method. Internal method that // creates a new auxiliary. void addAuxiliaryInner(); + //Resets the played timer in the track a just switched. + void resetTrack(Deck *deck); // Used to protect access to PlayerManager state across threads. mutable QMutex m_mutex; + private: + UserSettingsPointer m_pConfig; SoundManager* m_pSoundManager; EffectsManager* m_pEffectsManager; EngineMaster* m_pEngine; SamplerBank* m_pSamplerBank; AnalyzerQueue* m_pAnalyzerQueue; + ScrobblingManager m_scrobblingManager; ControlObject* m_pCONumDecks; ControlObject* m_pCONumSamplers; ControlObject* m_pCONumPreviewDecks; diff --git a/src/mixxx.cpp b/src/mixxx.cpp index 9322c74ee59..a4937e348c4 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -237,8 +237,7 @@ void MixxxMainWindow::initialize(QApplication* pApp, const CmdlineArgs& args) { #endif // Create the player manager. (long) - m_pPlayerManager = new PlayerManager(pConfig, m_pSoundManager, - m_pEffectsManager, m_pEngine); + m_pPlayerManager = new PlayerManager(pConfig, m_pSoundManager, m_pEffectsManager, m_pEngine, this); connect(m_pPlayerManager, SIGNAL(noMicrophoneInputConfigured()), this, SLOT(slotNoMicrophoneInputConfigured())); connect(m_pPlayerManager, SIGNAL(noDeckPassthroughInputConfigured()), @@ -408,6 +407,8 @@ void MixxxMainWindow::initialize(QApplication* pApp, const CmdlineArgs& args) { // immediately replaced by the real widget. launchProgress(100); + emit componentsInitialized(); + // Check direct rendering and warn user if they don't have it if (!CmdlineArgs::Instance().getSafeMode()) { checkDirectRendering(); diff --git a/src/mixxx.h b/src/mixxx.h index 4559890144b..769abf6ba3d 100644 --- a/src/mixxx.h +++ b/src/mixxx.h @@ -107,6 +107,7 @@ class MixxxMainWindow : public QMainWindow { void developerToolsDlgClosed(int r); void closeDeveloperToolsDlgChecked(int r); void fullScreenChanged(bool fullscreen); + void componentsInitialized(); protected: // Event filter to block certain events (eg. tooltips if tooltips are disabled) diff --git a/src/preferences/broadcastsettings.h b/src/preferences/broadcastsettings.h index 61c1f6c47bb..26574777601 100644 --- a/src/preferences/broadcastsettings.h +++ b/src/preferences/broadcastsettings.h @@ -21,7 +21,6 @@ class BroadcastSettings : public QObject { BroadcastProfilePtr createProfile(const QString& profileName); QList profiles(); BroadcastProfilePtr profileAt(int index); - void applyModel(BroadcastSettingsModel* pModel); signals: diff --git a/src/preferences/dialog/dlgfilelistenerbox.ui b/src/preferences/dialog/dlgfilelistenerbox.ui new file mode 100644 index 00000000000..92850c66a6f --- /dev/null +++ b/src/preferences/dialog/dlgfilelistenerbox.ui @@ -0,0 +1,80 @@ + + + fileListenerBox + + + + 0 + 0 + 549 + 399 + + + + GroupBox + + + File listener options + + + + + + + + QComboBox::AdjustToContentsOnFirstShow + + + + + + + + + + + Custom format + + + + + + + + + + + + + + Choose file path + + + + + + + + + + + + + + checkBox + toggled(bool) + comboBox + setDisabled(bool) + + + 82 + 208 + + + 504 + 116 + + + + + diff --git a/src/preferences/dialog/dlgprefbroadcast.cpp b/src/preferences/dialog/dlgprefbroadcast.cpp index dcd29daa575..10a96b5c4c4 100644 --- a/src/preferences/dialog/dlgprefbroadcast.cpp +++ b/src/preferences/dialog/dlgprefbroadcast.cpp @@ -5,6 +5,7 @@ #include #include #include +#include // shout.h checks for WIN32 to see if we are on Windows #ifdef WIN64 @@ -22,6 +23,7 @@ #include "preferences/dialog/dlgprefbroadcast.h" #include "encoder/encodersettings.h" #include "util/logger.h" +#include "preferences/configobject.h" namespace { const char* kSettingsGroupHeader = "Settings for %1"; @@ -143,9 +145,6 @@ DlgPrefBroadcast::~DlgPrefBroadcast() { delete m_pSettingsModel; } -void DlgPrefBroadcast::slotResetToDefaults() { -} - void DlgPrefBroadcast::slotUpdate() { updateModel(); connectOnApply->setChecked(false); diff --git a/src/preferences/dialog/dlgprefbroadcast.h b/src/preferences/dialog/dlgprefbroadcast.h index 3848d3d7d62..4a0c2ef2968 100644 --- a/src/preferences/dialog/dlgprefbroadcast.h +++ b/src/preferences/dialog/dlgprefbroadcast.h @@ -23,9 +23,8 @@ class DlgPrefBroadcast : public DlgPreferencePage, public Ui::DlgPrefBroadcastDl public slots: /** Apply changes to widget */ - void slotApply(); - void slotUpdate(); - void slotResetToDefaults(); + void slotApply() override; + void slotUpdate() override; void broadcastEnabledChanged(double value); void checkBoxEnableReconnectChanged(int value); void checkBoxLimitReconnectsChanged(int value); diff --git a/src/preferences/dialog/dlgpreferences.cpp b/src/preferences/dialog/dlgpreferences.cpp index dae9781b8eb..2d7b0611797 100644 --- a/src/preferences/dialog/dlgpreferences.cpp +++ b/src/preferences/dialog/dlgpreferences.cpp @@ -28,6 +28,7 @@ #include "preferences/dialog/dlgprefsound.h" #include "preferences/dialog/dlgpreflibrary.h" +#include "preferences/dialog/dlgprefmetadata.h" #include "controllers/dlgprefcontrollers.h" #ifdef __VINYLCONTROL__ @@ -134,6 +135,8 @@ DlgPreferences::DlgPreferences(MixxxMainWindow * mixxx, SkinLoader* pSkinLoader, pSettingsManager->broadcastSettings()); addPageWidget(m_broadcastingPage); #endif + m_metadataPage = new DlgPrefMetadata(this,m_pConfig); + addPageWidget(m_metadataPage); m_recordingPage = new DlgPrefRecord(this, m_pConfig); addPageWidget(m_recordingPage); @@ -270,6 +273,12 @@ void DlgPreferences::createIcons() { m_pBroadcastButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); #endif + m_pMetadataButton = new QTreeWidgetItem(contentsTreeWidget, QTreeWidgetItem::Type); + m_pMetadataButton->setIcon(0, QIcon(":/images/preferences/ic_preferences_broadcast.png")); + m_pMetadataButton->setText(0, tr("Metadata Broadcast")); + m_pMetadataButton->setTextAlignment(0, Qt::AlignLeft | Qt::AlignVCenter); + m_pMetadataButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + m_pRecordingButton = new QTreeWidgetItem(contentsTreeWidget, QTreeWidgetItem::Type); m_pRecordingButton->setIcon(0, QIcon(":/images/preferences/ic_preferences_recording.png")); m_pRecordingButton->setText(0, tr("Recording")); @@ -348,6 +357,8 @@ void DlgPreferences::changePage(QTreeWidgetItem* current, QTreeWidgetItem* previ } else if (current == m_pBroadcastButton) { switchToPage(m_broadcastingPage); #endif + } else if (current == m_pMetadataButton) { + switchToPage(m_metadataPage); } else if (current == m_pRecordingButton) { switchToPage(m_recordingPage); } else if (current == m_pBeatDetectionButton) { diff --git a/src/preferences/dialog/dlgpreferences.h b/src/preferences/dialog/dlgpreferences.h index d03ec00dae5..c3610552fa6 100644 --- a/src/preferences/dialog/dlgpreferences.h +++ b/src/preferences/dialog/dlgpreferences.h @@ -45,6 +45,7 @@ class DlgPrefEffects; class DlgPrefCrossfader; class DlgPrefAutoDJ; class DlgPrefBroadcast; +class DlgPrefMetadata; class DlgPrefRecord; class DlgPrefBeats; class DlgPrefKey; @@ -120,6 +121,7 @@ class DlgPreferences : public QDialog, public Ui::DlgPreferencesDlg { DlgPrefEffects* m_effectsPage; DlgPrefAutoDJ* m_autoDjPage; DlgPrefBroadcast* m_broadcastingPage; + DlgPrefMetadata* m_metadataPage; DlgPrefRecord* m_recordingPage; DlgPrefBeats* m_beatgridPage; DlgPrefKey* m_musicalKeyPage; @@ -147,6 +149,7 @@ class DlgPreferences : public QDialog, public Ui::DlgPreferencesDlg { //QTreeWidgetItem* m_pEffectsButton; QTreeWidgetItem* m_pAutoDJButton; QTreeWidgetItem* m_pBroadcastButton; + QTreeWidgetItem *m_pMetadataButton; QTreeWidgetItem* m_pRecordingButton; QTreeWidgetItem* m_pBeatDetectionButton; QTreeWidgetItem* m_pKeyDetectionButton; diff --git a/src/preferences/dialog/dlgprefmetadata.cpp b/src/preferences/dialog/dlgprefmetadata.cpp new file mode 100644 index 00000000000..ac609dd54f5 --- /dev/null +++ b/src/preferences/dialog/dlgprefmetadata.cpp @@ -0,0 +1,65 @@ + +#include "preferences/dialog/dlgprefmetadata.h" + +DlgPrefMetadata::DlgPrefMetadata(QWidget *pParent,UserSettingsPointer pSettings) + : DlgPreferencePage(pParent), + m_pSettings(pSettings), + m_pFileSettings(nullptr), + m_pListenBrainzSettings(nullptr) { + setupUi(this); + setFileSettings(); + setListenBrainzSettings(); +} + +void DlgPrefMetadata::setFileSettings() { + FileWidgets widgets; + widgets.enableCheckbox = enableFileListener; + widgets.encodingBox = fileEncodingComboBox; + widgets.formatLineEdit = fileFormatLineEdit; + widgets.filePathLineEdit = filePathLineEdit; + widgets.changeFilePathButton = filePathButton; + + m_pFileSettings = new MetadataFileSettings(m_pSettings,widgets,this); +} + +void DlgPrefMetadata::setListenBrainzSettings() { + ListenBrainzWidgets widgets; + widgets.m_pEnabled = enableListenbrainzBox; + widgets.m_pUserToken = listenBrainzUserTokenLineEdit; + m_pListenBrainzSettings = new ListenBrainzSettingsManager(m_pSettings,widgets); +} + +void DlgPrefMetadata::slotApply() { + m_pFileSettings->applySettings(); + m_pListenBrainzSettings->applySettings(); +} + +void DlgPrefMetadata::slotCancel() { + m_pFileSettings->cancelSettings(); + m_pListenBrainzSettings->cancelSettings(); +} + +void DlgPrefMetadata::slotResetToDefaults() { + m_pFileSettings->setSettingsToDefault(); + m_pListenBrainzSettings->setSettingsToDefault(); +} + +DlgPrefMetadata::~DlgPrefMetadata() { + delete m_pFileSettings; + delete m_pListenBrainzSettings; +} + + + + + + + + + + + + + + + diff --git a/src/preferences/dialog/dlgprefmetadata.h b/src/preferences/dialog/dlgprefmetadata.h new file mode 100644 index 00000000000..339e712b514 --- /dev/null +++ b/src/preferences/dialog/dlgprefmetadata.h @@ -0,0 +1,44 @@ +#pragma once + +#include "broadcast/scrobblingservice.h" +#include "preferences/dlgpreferencepage.h" +#include "preferences/dialog/ui_dlgprefmetadatadlg.h" +#include "preferences/metadatafilesettings.h" +#include "preferences/listenbrainzsettings.h" +#include "preferences/usersettings.h" + +namespace Ui { + class fileListenerBox; +} + +namespace { + + + const ConfigKey kListenbrainzEnabled = + ConfigKey("[Livemetadata]","ListenbrainzEnabled"); + + +}; + + + +class DlgPrefMetadata : public DlgPreferencePage, public Ui::DlgPrefMetadataDlg { + Q_OBJECT + public: + DlgPrefMetadata(QWidget *pParent, UserSettingsPointer pSettings); + ~DlgPrefMetadata(); + public slots: + void slotApply() override; + void slotCancel() override; + void slotResetToDefaults() override; + private: + UserSettingsPointer m_pSettings; + MetadataFileSettings *m_pFileSettings; + ListenBrainzSettingsManager *m_pListenBrainzSettings; + void setFileSettings(); + + void setListenBrainzSettings(); +}; + + + diff --git a/src/preferences/dialog/dlgprefmetadatadlg.ui b/src/preferences/dialog/dlgprefmetadatadlg.ui new file mode 100644 index 00000000000..083b6b9de70 --- /dev/null +++ b/src/preferences/dialog/dlgprefmetadatadlg.ui @@ -0,0 +1,257 @@ + + + DlgPrefMetadataDlg + + + + 0 + 0 + 668 + 514 + + + + Metadata Broadcast Preferences + + + + + + Metadata file options + + + + + + Enable metadata file + + + + + + + + + File encoding + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + false + + + + 0 + 0 + + + + + 200 + 0 + + + + false + + + QComboBox::NoInsert + + + QComboBox::AdjustToContents + + + + + + + + + File format. The strings $artist and $title will be substituted by the metadata of the current track. + + + + + + + false + + + + + + + + + false + + + Choose file path + + + + + + + false + + + + + + + + + + + + ListenBrainz options + + + + + + Enable ListenBrainz metadata broadcast + + + + + + + User token. If you don't have one please register <a href="https://listenbrainz.org/login/">here</a> + + + Qt::RichText + + + true + + + + + + + false + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + enableListenbrainzBox + toggled(bool) + listenBrainzUserTokenLineEdit + setEnabled(bool) + + + 142 + 430 + + + 123 + 471 + + + + + enableFileListener + toggled(bool) + fileFormatLineEdit + setEnabled(bool) + + + 121 + 51 + + + 121 + 142 + + + + + enableFileListener + toggled(bool) + filePathButton + setEnabled(bool) + + + 217 + 57 + + + 71 + 178 + + + + + enableFileListener + toggled(bool) + filePathLineEdit + setEnabled(bool) + + + 335 + 60 + + + 327 + 194 + + + + + enableFileListener + toggled(bool) + fileEncodingComboBox + setEnabled(bool) + + + 568 + 51 + + + 616 + 82 + + + + + diff --git a/src/preferences/listenbrainzsettings.cpp b/src/preferences/listenbrainzsettings.cpp new file mode 100644 index 00000000000..b319f6e2177 --- /dev/null +++ b/src/preferences/listenbrainzsettings.cpp @@ -0,0 +1,76 @@ + + +#include "preferences/listenbrainzsettings.h" + +ListenBrainzSettings ListenBrainzSettingsManager::s_latestSettings; + +ListenBrainzSettingsManager::ListenBrainzSettingsManager( + UserSettingsPointer pSettings, + const ListenBrainzWidgets& widgets) + : m_widgets(widgets), + m_pUserSettings(pSettings), + m_CPSettingsChanged(kListenBrainzSettingsChanged) { + s_latestSettings = getPersistedSettings(pSettings); + setUpWidgets(); +} + +ListenBrainzSettings ListenBrainzSettingsManager::getPersistedSettings(UserSettingsPointer pSettings) { + ListenBrainzSettings ret; + ret.enabled = pSettings->getValue(kListenBrainzEnabled,defaultListenBrainzEnabled); + ret.userToken = pSettings->getValue(kListenBrainzUserToken,QString()); + return ret; +} + +void ListenBrainzSettingsManager::setUpWidgets() { + m_widgets.m_pEnabled->setChecked(s_latestSettings.enabled); + if (!s_latestSettings.userToken.isEmpty()) { + m_widgets.m_pUserToken->setText(s_latestSettings.userToken); + } +} + +ListenBrainzSettings ListenBrainzSettingsManager::getLatestSettings() { + return s_latestSettings; +} + + +void ListenBrainzSettingsManager::applySettings() { + if (settingsDifferent() && settingsCorrect()) { + updateLatestSettingsAndNotify(); + persistSettings(); + } +} + +bool ListenBrainzSettingsManager::settingsDifferent() { + return s_latestSettings.enabled != m_widgets.m_pEnabled->isChecked() || + s_latestSettings.userToken != m_widgets.m_pUserToken->text(); +} + +void ListenBrainzSettingsManager::updateLatestSettingsAndNotify() { + s_latestSettings.enabled = m_widgets.m_pEnabled->isChecked(); + s_latestSettings.userToken = m_widgets.m_pUserToken->text(); + m_CPSettingsChanged.set(true); +} + +void ListenBrainzSettingsManager::persistSettings() { + m_pUserSettings->setValue(kListenBrainzEnabled,s_latestSettings.enabled); + m_pUserSettings->setValue(kListenBrainzUserToken,s_latestSettings.userToken); +} + +void ListenBrainzSettingsManager::cancelSettings() { + setUpWidgets(); +} + +void ListenBrainzSettingsManager::setSettingsToDefault() { + resetSettingsToDefault(); + setUpWidgets(); +} + +void ListenBrainzSettingsManager::resetSettingsToDefault() { + s_latestSettings.enabled = defaultListenBrainzEnabled; + s_latestSettings.userToken = QString(); +} + +bool ListenBrainzSettingsManager::settingsCorrect() { + return !m_widgets.m_pEnabled->isChecked() || + !m_widgets.m_pUserToken->text().isEmpty(); +} diff --git a/src/preferences/listenbrainzsettings.h b/src/preferences/listenbrainzsettings.h new file mode 100644 index 00000000000..455c3c3f43f --- /dev/null +++ b/src/preferences/listenbrainzsettings.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +#include "preferences/usersettings.h" +#include "control/controlproxy.h" + +namespace { + const ConfigKey kListenBrainzEnabled("[Livemetadata]","ListenBrainzEnabled"); + const ConfigKey kListenBrainzUserToken("[Livemetadata]","ListenBrainzUserToken"); + const ConfigKey kListenBrainzSettingsChanged("[Livemetadata]","ListenBrainzSettingsChanged"); + const bool defaultListenBrainzEnabled = false; +} + +struct ListenBrainzWidgets { + QCheckBox *m_pEnabled; + QLineEdit *m_pUserToken; +}; + +struct ListenBrainzSettings { + bool enabled; + QString userToken; +}; + +class ListenBrainzSettingsManager : public QObject { + Q_OBJECT + public: + ListenBrainzSettingsManager(UserSettingsPointer pSettings, const ListenBrainzWidgets& widgets); + static ListenBrainzSettings getPersistedSettings(UserSettingsPointer pSettings); + static ListenBrainzSettings getLatestSettings(); + void applySettings(); + void cancelSettings(); + void setSettingsToDefault(); + private: + void setUpWidgets(); + bool settingsDifferent(); + bool settingsCorrect(); + void updateLatestSettingsAndNotify(); + void persistSettings(); + void resetSettingsToDefault(); + private: + ListenBrainzWidgets m_widgets; + UserSettingsPointer m_pUserSettings; + static ListenBrainzSettings s_latestSettings; + ControlProxy m_CPSettingsChanged; +}; diff --git a/src/preferences/metadatafilesettings.cpp b/src/preferences/metadatafilesettings.cpp new file mode 100644 index 00000000000..0681b9b3593 --- /dev/null +++ b/src/preferences/metadatafilesettings.cpp @@ -0,0 +1,166 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "metadatafilesettings.h" + + +FileSettings MetadataFileSettings::s_latestSettings; + +MetadataFileSettings::MetadataFileSettings(UserSettingsPointer pSettings, + const FileWidgets& widgets, + QWidget *dialogWidget) + : m_pSettings(pSettings), + m_CPSettingsChanged(kFileSettingsChanged), + m_widgets(widgets), + m_pDialogWidget(dialogWidget), + m_fileEncodings{ + "UTF-8", + "latin1", + "Windows-1251", + "Windows-1252", + "Shift-JIS", + "GB18030", + "EUC-KR", + "EUC-JP"} { + s_latestSettings = getPersistedSettings(pSettings); + setupWidgets(); +} + +FileSettings MetadataFileSettings::getPersistedSettings(const UserSettingsPointer& pSettings) { + FileSettings ret; + ret.enabled = + pSettings->getValue(kMetadataFileEnabled,defaultFileMetadataEnabled); + ret.fileEncoding = + pSettings->getValue(kFileEncoding,defaultEncoding.constData()).toUtf8(); + ret.fileFormatString = + pSettings->getValue(kFileFormatString,defaultFileFormatString); + ret.filePath = + pSettings->getValue(kFilePath,defaultFilePath); + return ret; +} + +void MetadataFileSettings::setupWidgets() { + m_widgets.enableCheckbox->setChecked(s_latestSettings.enabled); + + setupEncodingComboBox(); + + m_widgets.formatLineEdit->setText(s_latestSettings.fileFormatString); + + m_widgets.filePathLineEdit->setText(s_latestSettings.filePath); + m_widgets.filePathLineEdit->setStyleSheet(""); + QObject::connect(m_widgets.changeFilePathButton,SIGNAL(pressed()), + this,SLOT(slotFilepathButtonClicked())); + +} + +FileSettings MetadataFileSettings::getLatestSettings() { + return MetadataFileSettings::s_latestSettings; +} + +void MetadataFileSettings::applySettings() { + if (fileSettingsDifferent() && checkIfSettingsCorrect()) { + updateLatestSettingsAndNotify(); + persistSettings(); + } +} + + + +bool MetadataFileSettings::fileSettingsDifferent() { + return s_latestSettings.enabled != + m_widgets.enableCheckbox->isChecked() || + + s_latestSettings.fileEncoding != + m_widgets.encodingBox->currentText() || + + s_latestSettings.fileFormatString != + m_widgets.formatLineEdit->text() || + + s_latestSettings.filePath != m_widgets.filePathLineEdit->text(); +} + +bool MetadataFileSettings::checkIfSettingsCorrect() { + QString supposedPath = m_widgets.filePathLineEdit->text(); + int lastIndex = supposedPath.lastIndexOf('/'); + if (lastIndex != -1) { + QString supposedDir = supposedPath.left(lastIndex); + QDir dir(supposedDir); + bool dirExists = dir.exists(); + if (!dirExists) { + m_widgets.filePathLineEdit->setStyleSheet("border: 1px solid red"); + } + else { + m_widgets.filePathLineEdit->setStyleSheet(""); + } + return dirExists; + } + return true; +} + +void MetadataFileSettings::updateLatestSettingsAndNotify() { + FileSettings ret; + ret.enabled = m_widgets.enableCheckbox->isChecked(); + ret.fileEncoding = m_widgets.encodingBox->currentText().toUtf8(); + ret.fileFormatString = m_widgets.formatLineEdit->text(); + ret.filePath = QDir(m_widgets.filePathLineEdit->text()).absolutePath(); + s_latestSettings = ret; + m_CPSettingsChanged.set(true); +} + +void MetadataFileSettings::persistSettings() { + m_pSettings->setValue(kMetadataFileEnabled,s_latestSettings.enabled); + m_pSettings->setValue(kFileEncoding,QString(s_latestSettings.fileEncoding)); + m_pSettings->setValue(kFileFormatString,s_latestSettings.fileFormatString); + m_pSettings->setValue(kFilePath,s_latestSettings.filePath); +} + +void MetadataFileSettings::setSettingsToDefault() { + resetSettingsToDefault(); + setupWidgets(); +} + +void MetadataFileSettings::resetSettingsToDefault() { + s_latestSettings.enabled = defaultFileMetadataEnabled; + s_latestSettings.fileEncoding = defaultEncoding; + s_latestSettings.fileFormatString = defaultFileFormatString; + s_latestSettings.filePath = defaultFilePath; +} + +void MetadataFileSettings::slotFilepathButtonClicked() { + QString newFilePath = QFileDialog::getSaveFileName( + m_pDialogWidget, + "Choose new file path", + checkIfSettingsCorrect() ? + m_widgets.filePathLineEdit->text() : + defaultFilePath, + "Text files(*.txt)" + ); + m_widgets.filePathLineEdit->setText(newFilePath); +} + + + +void MetadataFileSettings::cancelSettings() { + setupWidgets(); +} + +void MetadataFileSettings::setupEncodingComboBox() { + m_widgets.encodingBox->clear(); + + for (const QByteArray& fileEncoding : m_fileEncodings) { + DEBUG_ASSERT(QTextCodec::codecForName(fileEncoding) != nullptr); + m_widgets.encodingBox->addItem(fileEncoding); + } + + if (!m_fileEncodings.contains(QTextCodec::codecForLocale()->name())) { + m_widgets.encodingBox->addItem(QTextCodec::codecForLocale()->name()); + DEBUG_ASSERT(QTextCodec::codecForName(QTextCodec::codecForLocale()->name()) != nullptr); + } + + m_widgets.encodingBox->setCurrentText(s_latestSettings.fileEncoding); +} diff --git a/src/preferences/metadatafilesettings.h b/src/preferences/metadatafilesettings.h new file mode 100644 index 00000000000..c09fdd5dbee --- /dev/null +++ b/src/preferences/metadatafilesettings.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "control/controlproxy.h" +#include "preferences/usersettings.h" + +namespace { + const ConfigKey kMetadataFileEnabled = + ConfigKey("[Livemetadata]","MetadataFileEnabled"); + + const ConfigKey kFileEncoding = + ConfigKey("[Livemetadata]","FileEncoding"); + + const ConfigKey kFileFormatString = + ConfigKey("[Livemetadata]","FileFormatString"); + + const ConfigKey kFilePath = + ConfigKey("[Livemetadata]","CustomFormatString"); + const ConfigKey kFileSettingsChanged = + ConfigKey("[Livemetadata]","FileSettingsChanged"); + + const bool defaultFileMetadataEnabled = false; + const QByteArray defaultEncoding = "UTF-8"; + const QString defaultFilePath = QDir::currentPath() + "/NowPlaying.txt"; + const QString defaultFileFormatString = "$author - $title"; +} + +struct FileSettings { + bool enabled; + QByteArray fileEncoding; + QString fileFormatString, filePath; +}; + +struct FileWidgets { + QCheckBox *enableCheckbox; + QComboBox *encodingBox; + QLineEdit *formatLineEdit, + *filePathLineEdit; + QPushButton *changeFilePathButton; +}; + +class MetadataFileSettings : public QObject { + Q_OBJECT + public: + MetadataFileSettings(UserSettingsPointer pSettings, + const FileWidgets& widgets, + QWidget *dialogWidget); + ~MetadataFileSettings() = default; + static FileSettings getLatestSettings(); + static FileSettings getPersistedSettings(const UserSettingsPointer& pSettings); + void applySettings(); + void cancelSettings(); + void setSettingsToDefault(); + private: + void setupWidgets(); + void setupEncodingComboBox(); + void updateLatestSettingsAndNotify(); + void persistSettings(); + void resetSettingsToDefault(); + bool fileSettingsDifferent(); + bool checkIfSettingsCorrect(); + + UserSettingsPointer m_pSettings; + ControlProxy m_CPSettingsChanged; + static FileSettings s_latestSettings; + FileWidgets m_widgets; + QWidget *m_pDialogWidget; + + const QSet m_fileEncodings; + + private slots: + void slotFilepathButtonClicked(); +}; + + + + + diff --git a/src/test/scrobblingmanager_test.cpp b/src/test/scrobblingmanager_test.cpp new file mode 100644 index 00000000000..47ef449db99 --- /dev/null +++ b/src/test/scrobblingmanager_test.cpp @@ -0,0 +1,201 @@ +#include +#include +#include +#include + +#include "broadcast/scrobblingmanager.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "mixer/basetrackplayer.h" +#include "mixer/deck.h" +#include "mixer/playermanager.h" +#include "test/scrobblingmanager_test.h" +#include "track/track.h" +#include "track/trackplaytimers.h" + +using testing::_; + +class PlayerManagerMock : public PlayerManagerInterface { + public: + ~PlayerManagerMock() = default; + MOCK_CONST_METHOD1(getPlayer,BaseTrackPlayer*(QString)); + MOCK_CONST_METHOD1(getDeck,Deck*(unsigned int)); + MOCK_CONST_METHOD0(numberOfDecks,unsigned int()); + MOCK_CONST_METHOD1(getPreviewDeck,PreviewDeck*(unsigned int)); + MOCK_CONST_METHOD0(numberOfPreviewDecks,unsigned int()); + MOCK_CONST_METHOD1(getSampler,Sampler*(unsigned int)); + MOCK_CONST_METHOD0(numberOfSamplers,unsigned int()); +}; + +class ElapsedTimerMock : public TrackTimers::ElapsedTimer { + public: + ~ElapsedTimerMock() override = default; + MOCK_METHOD0(invalidate,void()); + MOCK_CONST_METHOD0(isValid,bool()); + MOCK_METHOD0(start,void()); + MOCK_CONST_METHOD0(elapsed,qint64()); +}; + +class AudibleStrategyMock : public TrackAudibleStrategy { + public: + ~AudibleStrategyMock() override = default; + MOCK_CONST_METHOD1(isPlayerAudible,bool(BaseTrackPlayer*)); +}; + +PlayerMock::PlayerMock(QObject* pParent, const QString& group) + : BaseTrackPlayer(pParent,group) {} + +class ScrobblingTest : public ::testing::Test { + public: + ScrobblingTest() + : playerManagerMock(new PlayerManagerMock), + scrobblingManager(playerManagerMock), + dummyPlayerLeft(nullptr,"DummyPlayerLeft"), + dummyPlayerRight(nullptr,"DummyPlayerRight"), + dummyTrackLeft(Track::newDummy(QFileInfo(),TrackId())), + dummyTrackRight(Track::newDummy(QFileInfo(),TrackId())), + timerScrobbler(new RegularTimerMock), + broadcastMock(new testing::NiceMock), + strategyMock(new AudibleStrategyMock) { + + scrobblingManager.setAudibleStrategy(strategyMock); + scrobblingManager.setMetadataBroadcaster(broadcastMock); + scrobblingManager.setTimer(timerScrobbler); + dummyTrackLeft->setDuration(120); + dummyTrackRight->setDuration(120); + //Set up left player + QObject::connect(&dummyPlayerLeft,&Deck::newTrackLoaded, + [this](TrackPointer pTrack)->void{ + scrobblingManager.slotNewTrackLoaded(pTrack,"DummyPlayerLeft");}); + QObject::connect(&dummyPlayerLeft,&Deck::trackResumed, + [this](TrackPointer pTrack)->void{ + scrobblingManager.slotTrackResumed(pTrack,"DummyPlayerLeft"); + }); + QObject::connect(&dummyPlayerLeft,&Deck::trackPaused, + &scrobblingManager,&ScrobblingManager::slotTrackPaused); + //Set up right player + QObject::connect(&dummyPlayerRight,&Deck::newTrackLoaded, + [this](TrackPointer pTrack)->void { + scrobblingManager.slotNewTrackLoaded(pTrack,"DummyPlayerRight");}); + QObject::connect(&dummyPlayerRight,&Deck::trackResumed, + [this](TrackPointer pTrack)->void{ + scrobblingManager.slotTrackResumed(pTrack,"DummyPlayerRight");}); + QObject::connect(&dummyPlayerRight,&Deck::trackPaused, + &scrobblingManager,&ScrobblingManager::slotTrackPaused); + EXPECT_CALL(*playerManagerMock,getPlayer(QString("DummyPlayerLeft"))) + .WillRepeatedly(testing::Return(&dummyPlayerLeft)); + EXPECT_CALL(*playerManagerMock,getPlayer(QString("DummyPlayerRight"))) + .WillRepeatedly(testing::Return(&dummyPlayerRight)); + } + + ~ScrobblingTest() { + delete playerManagerMock; + } + + PlayerManagerMock* playerManagerMock; + ScrobblingManager scrobblingManager; + PlayerMock dummyPlayerLeft, dummyPlayerRight; + TrackPointer dummyTrackLeft, dummyTrackRight; + RegularTimerMock *timerScrobbler; + MetadataBroadcasterMock *broadcastMock; + AudibleStrategyMock *strategyMock; +}; + + +//1 track, audible the whole time +TEST_F(ScrobblingTest,SingleTrackAudible) { + std::function(TrackPointer)> factory; + factory = [this] (TrackPointer pTrack) -> std::shared_ptr { + Q_UNUSED(pTrack); + std::shared_ptr + trackInfo(new TrackTimingInfo(pTrack)); + ElapsedTimerMock *etMock = new ElapsedTimerMock; + EXPECT_CALL(*etMock,invalidate()); + EXPECT_CALL(*etMock,isValid()) + .WillOnce(testing::Return(false)) + .WillOnce(testing::Return(true)); + EXPECT_CALL(*etMock,start()); + EXPECT_CALL(*etMock,elapsed()) + .WillOnce(testing::Return(60000)); + RegularTimerMock *tMock = new RegularTimerMock; + EXPECT_CALL(*tMock,start(1000)) + .WillOnce(testing::InvokeWithoutArgs( + trackInfo.get(), + &TrackTimingInfo::slotCheckIfScrobbable + )); + trackInfo->setTimer(tMock); + trackInfo->setElapsedTimer(etMock); + return trackInfo; + }; + scrobblingManager.setTrackInfoFactory(factory); + EXPECT_CALL(*strategyMock,isPlayerAudible(_)) + .WillOnce(testing::Return(true)); + EXPECT_CALL(*broadcastMock,slotAttemptScrobble(_)); + dummyPlayerLeft.emitTrackLoaded(dummyTrackLeft); + dummyPlayerLeft.emitTrackResumed(dummyTrackLeft); +} + +//1 Track, inaudible. +TEST_F(ScrobblingTest,SingleTrackInaudible) { + std::function(TrackPointer)> factory; + factory = [this] (TrackPointer pTrack) -> std::shared_ptr { + Q_UNUSED(pTrack); + std::shared_ptr + trackInfo(new TrackTimingInfo(pTrack)); + trackInfo->setTimer(new testing::NiceMock); + trackInfo->setElapsedTimer(new testing::NiceMock); + return trackInfo; + }; + scrobblingManager.setTrackInfoFactory(factory); + EXPECT_CALL(*strategyMock,isPlayerAudible(_)) + .WillOnce(testing::Return(false)); + dummyPlayerLeft.emitTrackLoaded(dummyTrackLeft); + dummyPlayerLeft.emitTrackResumed(dummyTrackLeft); + ASSERT_FALSE(scrobblingManager.hasScrobbledAnyTrack()); +} + +//Doesn't work because the two Id's are -1 and Scrobbling +// Manager stores ID's not TrackPointers. +TEST_F(ScrobblingTest,DISABLED_TwoTracksUnbalanced) { + std::function(TrackPointer)> factory; + factory = [this] (TrackPointer pTrack) -> std::shared_ptr { + if (pTrack == dummyTrackLeft) { + std::shared_ptr + trackInfo(new TrackTimingInfo(pTrack)); + trackInfo->setTimer(new testing::NiceMock); + trackInfo->setElapsedTimer(new testing::NiceMock); + return trackInfo; + } + else { + std::shared_ptr + trackInfo(new TrackTimingInfo(pTrack)); + ElapsedTimerMock *etMock = new ElapsedTimerMock; + EXPECT_CALL(*etMock,invalidate()); + EXPECT_CALL(*etMock,isValid()) + .WillOnce(testing::Return(false)) + .WillOnce(testing::Return(true)); + EXPECT_CALL(*etMock,start()); + EXPECT_CALL(*etMock,elapsed()) + .WillOnce(testing::Return(60000)); + RegularTimerMock *tMock = new RegularTimerMock; + EXPECT_CALL(*tMock,start(1000)) + .WillOnce(testing::InvokeWithoutArgs( + trackInfo.get(), + &TrackTimingInfo::slotCheckIfScrobbable + )); + trackInfo->setTimer(tMock); + trackInfo->setElapsedTimer(etMock); + return trackInfo; + } + }; + scrobblingManager.setTrackInfoFactory(factory); + EXPECT_CALL(*strategyMock,isPlayerAudible(&dummyPlayerLeft)) + .WillOnce(testing::Return(false)); + EXPECT_CALL(*strategyMock,isPlayerAudible(&dummyPlayerRight)) + .WillOnce(testing::Return(true)); + EXPECT_CALL(*broadcastMock,slotAttemptScrobble(dummyTrackRight)); + dummyPlayerLeft.emitTrackLoaded(dummyTrackLeft); + dummyPlayerRight.emitTrackLoaded(dummyTrackRight); + dummyPlayerLeft.emitTrackResumed(dummyTrackLeft); + dummyPlayerRight.emitTrackResumed(dummyTrackRight); +} \ No newline at end of file diff --git a/src/test/scrobblingmanager_test.h b/src/test/scrobblingmanager_test.h new file mode 100644 index 00000000000..402f06926fc --- /dev/null +++ b/src/test/scrobblingmanager_test.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include "broadcast/metadatabroadcast.h" +#include "gmock/gmock.h" +#include "mixer/basetrackplayer.h" + +class MetadataBroadcasterMock : public MetadataBroadcasterInterface { + Q_OBJECT + public: + ~MetadataBroadcasterMock() = default; + MOCK_METHOD1(slotNowListening,void(TrackPointer)); + MOCK_METHOD1(slotAttemptScrobble,void(TrackPointer)); + MOCK_METHOD0(slotAllTracksPaused,void()); + MetadataBroadcasterInterface& + addNewScrobblingService(const ScrobblingServicePtr &ptr) override { + return *this; + } + MOCK_METHOD1(newTrackLoaded,void(TrackPointer)); + MOCK_METHOD1(trackUnloaded,void(TrackPointer)); + MOCK_METHOD1(guiTick,void(double)); +}; + +class RegularTimerMock : public TrackTimers::RegularTimer { + Q_OBJECT + public: + ~RegularTimerMock() = default; + MOCK_METHOD1(start,void(double)); + MOCK_CONST_METHOD0(isActive,bool()); + MOCK_METHOD0(stop,void()); +}; + +class PlayerMock : public BaseTrackPlayer { + Q_OBJECT + public: + PlayerMock(QObject* pParent, const QString& group); + ~PlayerMock() = default; + MOCK_CONST_METHOD0(getLoadedTrack,TrackPointer()); + MOCK_METHOD2(slotLoadTrack,void(TrackPointer,bool)); + void emitTrackLoaded(TrackPointer pTrack) { + emit(newTrackLoaded(pTrack)); + } + void emitTrackResumed(TrackPointer pTrack) { + emit(trackResumed(pTrack)); + } + void emitTrackPaused(TrackPointer pTrack) { + emit(trackPaused(pTrack)); + } +}; diff --git a/src/test/trackplayedtimer_test.cpp b/src/test/trackplayedtimer_test.cpp new file mode 100644 index 00000000000..91d239d2355 --- /dev/null +++ b/src/test/trackplayedtimer_test.cpp @@ -0,0 +1,53 @@ +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "track/track.h" +#include "track/trackplaytimers.h" +#include "test/trackplayedtimer_test.h" +#include "track/tracktiminginfo.h" + +class ElapsedTimerMock : public TrackTimers::ElapsedTimer { + public: + ~ElapsedTimerMock() = default; + MOCK_METHOD0(invalidate,void()); + MOCK_METHOD0(start,void()); + MOCK_CONST_METHOD0(isValid,bool()); + MOCK_CONST_METHOD0(elapsed,qint64()); +}; + + +class DISABLED_TrackTimingInfoTest : public testing::Test { + public: + DISABLED_TrackTimingInfoTest() : + trackInfo(TrackPointer()) + { + testTrack = Track::newDummy(QFileInfo(),TrackId()); + trackInfo.setTrackPointer(testTrack); + } + ~DISABLED_TrackTimingInfoTest() = default; + TrackPointer testTrack; + TrackTimingInfo trackInfo; +}; + +TEST_F(DISABLED_TrackTimingInfoTest,SendsSignalWhenScrobbable) { + testTrack->setDuration(5); + //These have to be created in the heap otherwise + //we're deleting them twice. + ElapsedTimerMock *etmock = new ElapsedTimerMock(); + TimerMock *tmock = new TimerMock(); + EXPECT_CALL(*etmock,invalidate()) + .Times(2); + EXPECT_CALL(*etmock,isValid()) + .WillOnce(testing::Return(false)) + .WillOnce(testing::Return(true)); + EXPECT_CALL(*etmock,start()); + EXPECT_CALL(*tmock,start(1000)) + .WillOnce(testing::InvokeWithoutArgs(&trackInfo, + &TrackTimingInfo::slotCheckIfScrobbable)); + EXPECT_CALL(*etmock,elapsed()) + .WillOnce(testing::Return(2500)); + trackInfo.setTimer(tmock); + trackInfo.setElapsedTimer(etmock); + trackInfo.resetPlayedTime(); + trackInfo.resumePlayedTime(); + ASSERT_TRUE(trackInfo.isScrobbable()); +} \ No newline at end of file diff --git a/src/test/trackplayedtimer_test.h b/src/test/trackplayedtimer_test.h new file mode 100644 index 00000000000..8b145cffb40 --- /dev/null +++ b/src/test/trackplayedtimer_test.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include "track/trackplaytimers.h" +#include "gmock/gmock.h" + +class TimerMock : public TrackTimers::RegularTimer { + Q_OBJECT + public: + ~TimerMock() = default; + MOCK_METHOD1(start,void(double)); + MOCK_CONST_METHOD0(isActive,bool()); + MOCK_METHOD0(stop,void()); +}; \ No newline at end of file diff --git a/src/track/track.cpp b/src/track/track.cpp index 1da3ed8ee75..db270f749c2 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include @@ -1102,3 +1103,4 @@ Track::ExportMetadataResult Track::exportMetadata( return ExportMetadataResult::Failed; } } + diff --git a/src/track/track.h b/src/track/track.h index 21b65b3317f..c05d7284d1d 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -5,10 +5,13 @@ #include #include #include +#include +#include #include "library/dao/cue.h" #include "track/beats.h" #include "track/trackrecord.h" +#include "track/trackplaytimers.h" #include "util/memory.h" #include "util/sandbox.h" #include "waveform/waveform.h" @@ -22,6 +25,8 @@ class Track; typedef std::shared_ptr TrackPointer; typedef std::weak_ptr TrackWeakPointer; +struct Connection; + class Track : public QObject { Q_OBJECT @@ -139,7 +144,7 @@ class Track : public QObject { QString getDurationTextMilliseconds() const { return getDurationText(mixxx::Duration::Precision::MILLISECONDS); } - + // Set BPM double setBpm(double); // Returns BPM diff --git a/src/track/trackplaytimers.cpp b/src/track/trackplaytimers.cpp new file mode 100644 index 00000000000..deb04a7423c --- /dev/null +++ b/src/track/trackplaytimers.cpp @@ -0,0 +1,50 @@ +#include "track/trackplaytimers.h" + +TrackTimers::GUITickTimer::GUITickTimer() + : m_msSoFar(0.0), + m_msTarget(0.0), + m_isActive(false), + m_timeoutSent(false) { + +} + +void TrackTimers::GUITickTimer::start(double msec) { + m_msTarget = msec; + m_msSoFar = 0.0; + m_isActive = true; + m_timeoutSent = false; +} + +bool TrackTimers::GUITickTimer::isActive() const { + return m_isActive; +} + +void TrackTimers::GUITickTimer::stop() { + m_isActive = false; +} + +void TrackTimers::GUITickTimer::slotTick(double timeSinceLastTick) { + if (!m_timeoutSent && m_isActive) { + m_msSoFar += timeSinceLastTick; + if (m_msSoFar >= m_msTarget) { + m_timeoutSent = true; + emit timeout(); + } + } +} + +void TrackTimers::ElapsedTimerQt::invalidate() { + m_elapsedTimer.invalidate(); +} + +bool TrackTimers::ElapsedTimerQt::isValid() const { + return m_elapsedTimer.isValid(); +} + +void TrackTimers::ElapsedTimerQt::start() { + m_elapsedTimer.start(); +} + +qint64 TrackTimers::ElapsedTimerQt::elapsed() const { + return m_elapsedTimer.elapsed(); +} \ No newline at end of file diff --git a/src/track/trackplaytimers.h b/src/track/trackplaytimers.h new file mode 100644 index 00000000000..b2aeed44bcc --- /dev/null +++ b/src/track/trackplaytimers.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include "control/controlproxy.h" + +namespace TrackTimers { + class ElapsedTimer { + public: + ElapsedTimer() = default; + virtual ~ElapsedTimer() = default; + virtual void invalidate() = 0; + virtual bool isValid() const = 0; + virtual void start() = 0; + virtual qint64 elapsed() const = 0; + }; + + class RegularTimer : public QObject { + Q_OBJECT + public: + RegularTimer() = default; + virtual ~RegularTimer() = default; + virtual void start(double msec) = 0; + virtual bool isActive() const = 0; + public slots: + virtual void stop() = 0; + signals: + void timeout(); + }; + + class GUITickTimer : public RegularTimer { + Q_OBJECT + public: + GUITickTimer(); + ~GUITickTimer() override = default; + void start(double msec) override; + bool isActive() const override; + void stop() override; + private: + double m_msSoFar; + double m_msTarget; + bool m_isActive; + bool m_timeoutSent; + public slots: + void slotTick(double timeSinceLastTick); + }; + + class ElapsedTimerQt : public ElapsedTimer { + public: + ElapsedTimerQt() = default; + ~ElapsedTimerQt() override = default; + void invalidate() override; + bool isValid() const override; + void start() override; + qint64 elapsed() const override; + private: + QElapsedTimer m_elapsedTimer; + }; +} diff --git a/src/track/tracktiminginfo.cpp b/src/track/tracktiminginfo.cpp new file mode 100644 index 00000000000..750d4c9110a --- /dev/null +++ b/src/track/tracktiminginfo.cpp @@ -0,0 +1,90 @@ +#include "track/tracktiminginfo.h" + +TrackTimingInfo::TrackTimingInfo(TrackPointer pTrack) + : m_pElapsedTimer(new TrackTimers::ElapsedTimerQt()), + m_pTimer(new TrackTimers::GUITickTimer()), + m_pTrackPtr(pTrack), + m_playedMs(0), + m_isTrackScrobbable(false), + m_isTimerPaused(true) +{ + connect(m_pTimer.get(),SIGNAL(timeout()), + this,SLOT(slotCheckIfScrobbable())); + m_pElapsedTimer->invalidate(); +} + +void TrackTimingInfo::pausePlayedTime() { + if (m_pElapsedTimer->isValid()) { + m_playedMs += m_pElapsedTimer->elapsed(); + m_pElapsedTimer->invalidate(); + m_isTimerPaused = true; + } +} + +void TrackTimingInfo::resumePlayedTime() { + if (!m_pElapsedTimer->isValid()) { + m_pElapsedTimer->start(); + m_pTimer->start(1000); + m_isTimerPaused = false; + } +} + +bool TrackTimingInfo::isTimerPaused() const { + return m_isTimerPaused; +} + +void TrackTimingInfo::resetPlayedTime() { + m_pElapsedTimer->invalidate(); + m_isTimerPaused = true; + m_playedMs = 0; +} + +void TrackTimingInfo::setElapsedTimer(TrackTimers::ElapsedTimer *elapsedTimer) { + m_pElapsedTimer.reset(elapsedTimer); + m_pElapsedTimer->invalidate(); +} + +void TrackTimingInfo::setTimer(TrackTimers::RegularTimer *timer) { + m_pTimer.reset(timer); +} + +void TrackTimingInfo::slotCheckIfScrobbable() { + if (m_isTrackScrobbable) { + return; + } + qint64 msInTimer = 0; + if (m_pElapsedTimer->isValid()) + msInTimer = m_pElapsedTimer->elapsed(); + else + return; + if (!m_pTrackPtr) { + qDebug() << "Track pointer is null when checking if track is scrobbable"; + return; + } + if ((msInTimer + m_playedMs) / 1000.0 >= + m_pTrackPtr->getDurationInt() / 2.0 || + (msInTimer + m_playedMs) / 1000.0 >= 240.0) { + m_isTrackScrobbable = true; + emit readyToBeScrobbled(m_pTrackPtr); + } else { + m_pTimer->start(1000); + } +} + +void TrackTimingInfo::setMsPlayed(qint64 ms) { + m_playedMs = ms; +} + +bool TrackTimingInfo::isScrobbable() const { + return m_isTrackScrobbable; +} + +void TrackTimingInfo::setTrackPointer(TrackPointer pTrack) { + m_pTrackPtr = pTrack; +} + +void TrackTimingInfo::slotGuiTick(double timeSinceLastTick) { + TrackTimers::GUITickTimer *timer = + qobject_cast(m_pTimer.get()); + timer->slotTick(timeSinceLastTick); +} \ No newline at end of file diff --git a/src/track/tracktiminginfo.h b/src/track/tracktiminginfo.h new file mode 100644 index 00000000000..9dc55767de9 --- /dev/null +++ b/src/track/tracktiminginfo.h @@ -0,0 +1,30 @@ +#include "track/track.h" +#include "track/trackplaytimers.h" +#include + +class TrackTimingInfo : public QObject { + Q_OBJECT + public: + TrackTimingInfo(TrackPointer pTrack); + void pausePlayedTime(); + void resumePlayedTime(); + void resetPlayedTime(); + void setElapsedTimer(TrackTimers::ElapsedTimer *elapsedTimer); + void setTimer(TrackTimers::RegularTimer *timer); + void setMsPlayed(qint64 ms); + bool isScrobbable() const; + void setTrackPointer(TrackPointer pTrack); + bool isTimerPaused() const; + public slots: + void slotCheckIfScrobbable(); + void slotGuiTick(double timeSinceLastTick); + signals: + void readyToBeScrobbled(TrackPointer pTrack); + private: + std::unique_ptr m_pElapsedTimer; + std::unique_ptr m_pTimer; + TrackPointer m_pTrackPtr; + qint64 m_playedMs; + bool m_isTrackScrobbable; + bool m_isTimerPaused; +}; \ No newline at end of file