Repository: mixxxdj/mixxx
Pull request #14516: feat: refactor player proxy and create separated track proxy
Author: login: acolombier
Labels: name: build
name: needs review
name: code quality
name: qml
Created: 2025-03-24T02:14:14Z | Merged: 2025-05-19T08:32:58Z

This allows QML to interact with a track without depending of the player


Diff:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 128de4c23187..9c3fb67dd3c1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -3436,24 +3436,25 @@ if(QML)
       src/qml/qmlapplication.cpp
       src/qml/qmlautoreload.cpp
       src/qml/qmlbeatsmodel.cpp
-      src/qml/qmlcuesmodel.cpp
-      src/qml/qmlcontrolproxy.cpp
+      src/qml/qmlchainpresetmodel.cpp
       src/qml/qmlconfigproxy.cpp
+      src/qml/qmlcontrolproxy.cpp
+      src/qml/qmlcuesmodel.cpp
       src/qml/qmldlgpreferencesproxy.cpp
       src/qml/qmleffectmanifestparametersmodel.cpp
-      src/qml/qmleffectsmanagerproxy.cpp
       src/qml/qmleffectslotproxy.cpp
+      src/qml/qmleffectsmanagerproxy.cpp
       src/qml/qmllibraryproxy.cpp
       src/qml/qmllibrarytracklistmodel.cpp
+      src/qml/qmlmixxxcontrollerscreen.cpp
       src/qml/qmlplayermanagerproxy.cpp
       src/qml/qmlplayerproxy.cpp
       src/qml/qmlvisibleeffectsmodel.cpp
-      src/qml/qmlchainpresetmodel.cpp
-      src/qml/qmlwaveformoverview.cpp
-      src/qml/qmlmixxxcontrollerscreen.cpp
       src/qml/qmlwaveformdisplay.cpp
+      src/qml/qmlwaveformoverview.cpp
       src/qml/qmlwaveformrenderer.cpp
       src/qml/qmlsettingparameter.cpp
+      src/qml/qmltrackproxy.cpp
       src/waveform/renderers/allshader/digitsrenderer.cpp
       src/waveform/renderers/allshader/waveformrenderbeat.cpp
       src/waveform/renderers/allshader/waveformrenderer.cpp
diff --git a/res/qml/Mixxx/Controls/WaveformOverview.qml b/res/qml/Mixxx/Controls/WaveformOverview.qml
index 1c217986451a..0ed78fea1562 100644
--- a/res/qml/Mixxx/Controls/WaveformOverview.qml
+++ b/res/qml/Mixxx/Controls/WaveformOverview.qml
@@ -6,8 +6,9 @@ Mixxx.WaveformOverview {
     id: root
 
     required property string group
+    readonly property var player: Mixxx.PlayerManager.getPlayer(root.group)
 
-    player: Mixxx.PlayerManager.getPlayer(root.group)
+    track: player.currentTrack
 
     Mixxx.ControlProxy {
         id: trackLoadedControl
diff --git a/src/coreservices.cpp b/src/coreservices.cpp
index c9d404c7b46f..040b9dad15b9 100644
--- a/src/coreservices.cpp
+++ b/src/coreservices.cpp
@@ -40,13 +40,9 @@
 
 #include "controllers/scripting/controllerscriptenginebase.h"
 #include "qml/qmlconfigproxy.h"
-#include "qml/qmlcontrolproxy.h"
-#include "qml/qmldlgpreferencesproxy.h"
-#include "qml/qmleffectslotproxy.h"
 #include "qml/qmleffectsmanagerproxy.h"
 #include "qml/qmllibraryproxy.h"
 #include "qml/qmlplayermanagerproxy.h"
-#include "qml/qmlplayerproxy.h"
 #endif
 #include "soundio/soundmanager.h"
 #include "sources/soundsourceproxy.h"
diff --git a/src/qml/qmlplayermanagerproxy.cpp b/src/qml/qmlplayermanagerproxy.cpp
index 2544b34f3ad5..cc2d1b3a8e97 100644
--- a/src/qml/qmlplayermanagerproxy.cpp
+++ b/src/qml/qmlplayermanagerproxy.cpp
@@ -5,6 +5,7 @@
 #include "mixer/playermanager.h"
 #include "moc_qmlplayermanagerproxy.cpp"
 #include "qml/qmlplayerproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
@@ -31,6 +32,20 @@ QmlPlayerProxy* QmlPlayerManagerProxy::getPlayer(const QString& group) {
             [this, group](const QString& trackLocation, bool play) {
                 loadLocationToPlayer(trackLocation, group, play);
             });
+    connect(pPlayerProxy,
+            &QmlPlayerProxy::loadTrackRequested,
+            this,
+            [this, group](TrackPointer track,
+#ifdef __STEM__
+                    mixxx::StemChannelSelection stemSelection,
+#endif
+                    bool play) {
+                loadTrackToPlayer(track, group,
+#ifdef __STEM__
+                        stemSelection,
+#endif
+                        play);
+            });
     connect(pPlayerProxy,
             &QmlPlayerProxy::cloneFromGroup,
             this,
@@ -59,6 +74,19 @@ void QmlPlayerManagerProxy::loadLocationToPlayer(
     m_pPlayerManager->slotLoadLocationToPlayer(location, group, play);
 }
 
+void QmlPlayerManagerProxy::loadTrackToPlayer(TrackPointer track,
+        const QString& group,
+#ifdef __STEM__
+        mixxx::StemChannelSelection stemSelection,
+#endif
+        bool play) {
+    m_pPlayerManager->slotLoadTrackToPlayer(track, group,
+#ifdef __STEM__
+            stemSelection,
+#endif
+            play);
+}
+
 // static
 QmlPlayerManagerProxy* QmlPlayerManagerProxy::create(QQmlEngine* pQmlEngine, QJSEngine* pJsEngine) {
     // The implementation of this method is mostly taken from the code example
diff --git a/src/qml/qmlplayermanagerproxy.h b/src/qml/qmlplayermanagerproxy.h
index 1cf650b7ceb8..3f23ad664867 100644
--- a/src/qml/qmlplayermanagerproxy.h
+++ b/src/qml/qmlplayermanagerproxy.h
@@ -24,6 +24,12 @@ class QmlPlayerManagerProxy : public QObject {
             const QUrl& locationUrl, bool play = false);
     Q_INVOKABLE void loadLocationToPlayer(
             const QString& location, const QString& group, bool play = false);
+    Q_INVOKABLE void loadTrackToPlayer(TrackPointer track,
+            const QString& group,
+#ifdef __STEM__
+            mixxx::StemChannelSelection stemSelection,
+#endif
+            bool play);
 
     static QmlPlayerManagerProxy* create(QQmlEngine* pQmlEngine, QJSEngine* pJsEngine);
     static void registerPlayerManager(std::shared_ptr<PlayerManager> pPlayerManager) {
diff --git a/src/qml/qmlplayerproxy.cpp b/src/qml/qmlplayerproxy.cpp
index d3b6056fda6b..ba5b58ea2b6d 100644
--- a/src/qml/qmlplayerproxy.cpp
+++ b/src/qml/qmlplayerproxy.cpp
@@ -1,42 +1,19 @@
 #include "qml/qmlplayerproxy.h"
 
 #include <QBuffer>
+#include <QQmlEngine>
 
 #include "mixer/basetrackplayer.h"
 #include "moc_qmlplayerproxy.cpp"
-#include "qml/asyncimageprovider.h"
-
-#define PROPERTY_IMPL_GETTER(TYPE, NAME, GETTER)     \
-    TYPE QmlPlayerProxy::GETTER() const {            \
-        const TrackPointer pTrack = m_pCurrentTrack; \
-        if (pTrack == nullptr) {                     \
-            return TYPE();                           \
-        }                                            \
-        return pTrack->GETTER();                     \
-    }
-
-#define PROPERTY_IMPL(TYPE, NAME, GETTER, SETTER)    \
-    PROPERTY_IMPL_GETTER(TYPE, NAME, GETTER)         \
-    void QmlPlayerProxy::SETTER(const TYPE& value) { \
-        const TrackPointer pTrack = m_pCurrentTrack; \
-        if (pTrack != nullptr) {                     \
-            pTrack->SETTER(value);                   \
-        }                                            \
-    }
+#include "qmltrackproxy.h"
+#include "track/track.h"
 
 namespace mixxx {
 namespace qml {
 
 QmlPlayerProxy::QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent)
         : QObject(parent),
-          m_pTrackPlayer(pTrackPlayer),
-          m_pBeatsModel(new QmlBeatsModel(this)),
-          m_pHotcuesModel(new QmlCuesModel(this))
-#ifdef __STEM__
-          ,
-          m_pStemsModel(std::make_unique<QmlStemsModel>(this))
-#endif
-{
+          m_pTrackPlayer(pTrackPlayer) {
     connect(m_pTrackPlayer,
             &BaseTrackPlayer::loadingTrack,
             this,
@@ -46,15 +23,25 @@ QmlPlayerProxy::QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent)
             this,
             &QmlPlayerProxy::slotTrackLoaded);
     connect(m_pTrackPlayer,
-            &BaseTrackPlayer::playerEmpty,
+            &BaseTrackPlayer::trackUnloaded,
             this,
-            &QmlPlayerProxy::trackUnloaded);
-    connect(this, &QmlPlayerProxy::trackChanged, this, &QmlPlayerProxy::slotTrackChanged);
+            &QmlPlayerProxy::slotTrackUnloaded);
     if (m_pTrackPlayer && m_pTrackPlayer->getLoadedTrack()) {
         slotTrackLoaded(pTrackPlayer->getLoadedTrack());
     }
 }
 
+void QmlPlayerProxy::loadTrack(QmlTrackProxy* track, bool play) {
+    if (track == nullptr || track->internal() == nullptr) {
+        return;
+    }
+    emit loadTrackRequested(track->internal(),
+#ifdef __STEM__
+            mixxx::StemChannel::All,
+#endif
+            play);
+}
+
 void QmlPlayerProxy::loadTrackFromLocation(const QString& trackLocation, bool play) {
     emit loadTrackFromLocationRequested(trackLocation, play);
 }
@@ -69,323 +56,47 @@ void QmlPlayerProxy::loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool
 
 void QmlPlayerProxy::slotTrackLoaded(TrackPointer pTrack) {
     m_pCurrentTrack = pTrack;
-    if (pTrack != nullptr) {
-        connect(pTrack.get(),
-                &Track::artistChanged,
-                this,
-                &QmlPlayerProxy::artistChanged);
-        connect(pTrack.get(),
-                &Track::titleChanged,
-                this,
-                &QmlPlayerProxy::titleChanged);
-        connect(pTrack.get(),
-                &Track::albumChanged,
-                this,
-                &QmlPlayerProxy::albumChanged);
-        connect(pTrack.get(),
-                &Track::albumArtistChanged,
-                this,
-                &QmlPlayerProxy::albumArtistChanged);
-        connect(pTrack.get(),
-                &Track::genreChanged,
-                this,
-                &QmlPlayerProxy::genreChanged);
-        connect(pTrack.get(),
-                &Track::composerChanged,
-                this,
-                &QmlPlayerProxy::composerChanged);
-        connect(pTrack.get(),
-                &Track::groupingChanged,
-                this,
-                &QmlPlayerProxy::groupingChanged);
-        connect(pTrack.get(),
-                &Track::yearChanged,
-                this,
-                &QmlPlayerProxy::yearChanged);
-        connect(pTrack.get(),
-                &Track::trackNumberChanged,
-                this,
-                &QmlPlayerProxy::trackNumberChanged);
-        connect(pTrack.get(),
-                &Track::trackTotalChanged,
-                this,
-                &QmlPlayerProxy::trackTotalChanged);
-        connect(pTrack.get(),
-                &Track::commentChanged,
-                this,
-                &QmlPlayerProxy::commentChanged);
-        connect(pTrack.get(),
-                &Track::keyChanged,
-                this,
-                &QmlPlayerProxy::keyTextChanged);
-        connect(pTrack.get(),
-                &Track::colorUpdated,
-                this,
-                &QmlPlayerProxy::colorChanged);
-        connect(pTrack.get(),
-                &Track::waveformUpdated,
-                this,
-                &QmlPlayerProxy::slotWaveformChanged);
-        connect(pTrack.get(),
-                &Track::beatsUpdated,
-                this,
-                &QmlPlayerProxy::slotBeatsChanged);
-        connect(pTrack.get(),
-                &Track::cuesUpdated,
-                this,
-                &QmlPlayerProxy::slotHotcuesChanged);
-#ifdef __STEM__
-        connect(pTrack.get(),
-                &Track::stemsUpdated,
-                this,
-                &QmlPlayerProxy::slotStemsChanged);
-#endif
-        slotBeatsChanged();
-        slotHotcuesChanged();
-#ifdef __STEM__
-        slotStemsChanged();
-#endif
-        slotWaveformChanged();
-    }
     emit trackChanged();
     emit trackLoaded();
 }
 
-void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) {
+void QmlPlayerProxy::slotTrackUnloaded(TrackPointer pOldTrack) {
     VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) {
         qWarning() << "QML Player proxy was expected to contain "
                    << pOldTrack.get() << "as active track but got"
                    << m_pCurrentTrack.get();
     }
-
-    if (pNewTrack.get() == m_pCurrentTrack.get()) {
-        emit trackLoading();
-        return;
-    }
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack != nullptr) {
-        disconnect(pTrack.get(), nullptr, this, nullptr);
+    if (m_pCurrentTrack != nullptr) {
+        disconnect(m_pCurrentTrack.get(), nullptr, this, nullptr);
     }
     m_pCurrentTrack.reset();
-    m_pCurrentTrack = pNewTrack;
-    m_waveformTexture = QImage();
     emit trackChanged();
-    emit trackLoading();
-}
-
-void QmlPlayerProxy::slotTrackChanged() {
-    emit artistChanged();
-    emit titleChanged();
-    emit albumChanged();
-    emit albumArtistChanged();
-    emit genreChanged();
-    emit composerChanged();
-    emit groupingChanged();
-    emit yearChanged();
-    emit trackNumberChanged();
-    emit trackTotalChanged();
-    emit commentChanged();
-    emit keyTextChanged();
-    emit colorChanged();
-    emit coverArtUrlChanged();
-    emit trackLocationUrlChanged();
-#ifdef __STEM__
-    emit stemsChanged();
-#endif
-
-    emit waveformLengthChanged();
-    emit waveformTextureChanged();
-    emit waveformTextureSizeChanged();
-    emit waveformTextureStrideChanged();
+    emit trackUnloaded();
 }
 
-void QmlPlayerProxy::slotWaveformChanged() {
-    emit waveformLengthChanged();
-    emit waveformTextureSizeChanged();
-    emit waveformTextureStrideChanged();
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (!pTrack) {
-        return;
-    }
-    const ConstWaveformPointer pWaveform =
-            pTrack->getWaveform();
-    if (!pWaveform) {
-        return;
-    }
-    const int textureWidth = pWaveform->getTextureStride();
-    const int textureHeight = pWaveform->getTextureSize() / pWaveform->getTextureStride();
-
-    const WaveformData* data = pWaveform->data();
-    // Make a copy of the waveform data, stripping the stems portion. Note that the datasize is
-    // different from the texture size -- we want the full texture size so the upload works. See
-    // m_data in waveform/waveform.h.
-    m_waveformData.resize(pWaveform->getTextureSize());
-    for (int i = 0; i < pWaveform->getDataSize(); i++) {
-        m_waveformData[i] = data[i].filtered;
-    }
-
-    m_waveformTexture =
-            QImage(reinterpret_cast<const uchar*>(m_waveformData.data()),
-                    textureWidth,
-                    textureHeight,
-                    QImage::Format_RGBA8888);
-    DEBUG_ASSERT(!m_waveformTexture.isNull());
-    emit waveformTextureChanged();
-}
-
-void QmlPlayerProxy::slotBeatsChanged() {
-    VERIFY_OR_DEBUG_ASSERT(m_pBeatsModel != nullptr) {
-        return;
-    }
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        const auto trackEndPosition = mixxx::audio::FramePos{
-                pTrack->getDuration() * pTrack->getSampleRate()};
-        const auto pBeats = pTrack->getBeats();
-        m_pBeatsModel->setBeats(pBeats, trackEndPosition);
-    } else {
-        m_pBeatsModel->setBeats(nullptr, audio::kStartFramePos);
-    }
+QmlTrackProxy* QmlPlayerProxy::currentTrack() {
+    auto* pTrack = new QmlTrackProxy(m_pCurrentTrack, this);
+    QQmlEngine::setObjectOwnership(pTrack, QQmlEngine::JavaScriptOwnership);
+    return pTrack;
 }
 
-#ifdef __STEM__
-void QmlPlayerProxy::slotStemsChanged() {
-    VERIFY_OR_DEBUG_ASSERT(m_pStemsModel != nullptr) {
-        return;
-    }
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        m_pStemsModel->setStems(pTrack->getStemInfo());
-        emit stemsChanged();
-    }
-}
-#endif
-
-void QmlPlayerProxy::slotHotcuesChanged() {
-    VERIFY_OR_DEBUG_ASSERT(m_pHotcuesModel != nullptr) {
+void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) {
+    if (pNewTrack.get() == m_pCurrentTrack.get()) {
+        emit trackLoading();
         return;
     }
 
-    QList<CuePointer> hotcues;
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        const auto& cuePoints = pTrack->getCuePoints();
-        for (const auto& cuePoint : cuePoints) {
-            if (cuePoint->getHotCue() == Cue::kNoHotCue)
-                continue;
-            hotcues.append(cuePoint);
-        }
+    if (m_pCurrentTrack != nullptr) {
+        disconnect(m_pCurrentTrack.get(), nullptr, this, nullptr);
     }
-    m_pHotcuesModel->setCues(hotcues);
-    emit cuesChanged();
-}
-
-int QmlPlayerProxy::getWaveformLength() const {
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        const ConstWaveformPointer pWaveform = pTrack->getWaveform();
-        if (pWaveform) {
-            return pWaveform->getDataSize();
-        }
-    }
-    return 0;
-}
-
-QString QmlPlayerProxy::getWaveformTexture() const {
-    if (m_waveformTexture.isNull()) {
-        return QString();
-    }
-    QByteArray byteArray;
-    QBuffer buffer(&byteArray);
-    buffer.open(QIODevice::WriteOnly);
-    m_waveformTexture.save(&buffer, "png");
-
-    QString imageData = QString::fromLatin1(byteArray.toBase64().data());
-    if (imageData.isEmpty()) {
-        return QString();
-    }
-
-    return QStringLiteral("data:image/png;base64,") + imageData;
-}
-
-int QmlPlayerProxy::getWaveformTextureSize() const {
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        const ConstWaveformPointer pWaveform = pTrack->getWaveform();
-        if (pWaveform) {
-            return pWaveform->getTextureSize();
-        }
-    }
-    return 0;
-}
-
-int QmlPlayerProxy::getWaveformTextureStride() const {
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        const ConstWaveformPointer pWaveform = pTrack->getWaveform();
-        if (pWaveform) {
-            return pWaveform->getTextureStride();
-        }
-    }
-    return 0;
+    m_pCurrentTrack = pNewTrack;
+    emit trackChanged();
+    emit trackLoading();
 }
 
 bool QmlPlayerProxy::isLoaded() const {
     return m_pCurrentTrack != nullptr;
 }
 
-PROPERTY_IMPL(QString, artist, getArtist, setArtist)
-PROPERTY_IMPL(QString, title, getTitle, setTitle)
-PROPERTY_IMPL(QString, album, getAlbum, setAlbum)
-PROPERTY_IMPL(QString, albumArtist, getAlbumArtist, setAlbumArtist)
-PROPERTY_IMPL_GETTER(QString, genre, getGenre)
-PROPERTY_IMPL(QString, composer, getComposer, setComposer)
-PROPERTY_IMPL(QString, grouping, getGrouping, setGrouping)
-PROPERTY_IMPL(QString, year, getYear, setYear)
-PROPERTY_IMPL(QString, trackNumber, getTrackNumber, setTrackNumber)
-PROPERTY_IMPL(QString, trackTotal, getTrackTotal, setTrackTotal)
-PROPERTY_IMPL(QString, comment, getComment, setComment)
-PROPERTY_IMPL(QString, keyText, getKeyText, setKeyText)
-
-QColor QmlPlayerProxy::getColor() const {
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack == nullptr) {
-        return QColor();
-    }
-    return RgbColor::toQColor(pTrack->getColor());
-}
-
-void QmlPlayerProxy::setColor(const QColor& value) {
-    const TrackPointer pTrack = m_pTrackPlayer->getLoadedTrack();
-    if (pTrack != nullptr) {
-        std::optional<RgbColor> color = RgbColor::fromQColor(value);
-        pTrack->setColor(color);
-    }
-}
-
-QUrl QmlPlayerProxy::getCoverArtUrl() const {
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack == nullptr) {
-        return QUrl();
-    }
-
-    const CoverInfo coverInfo = pTrack->getCoverInfoWithLocation();
-    return AsyncImageProvider::trackLocationToCoverArtUrl(coverInfo.trackLocation);
-}
-
-QUrl QmlPlayerProxy::getTrackLocationUrl() const {
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack == nullptr) {
-        return QUrl();
-    }
-
-    return QUrl::fromLocalFile(pTrack->getLocation());
-}
-
 } // namespace qml
 } // namespace mixxx
diff --git a/src/qml/qmlplayerproxy.h b/src/qml/qmlplayerproxy.h
index 54f42bb9c9a3..538639bdb0fa 100644
--- a/src/qml/qmlplayerproxy.h
+++ b/src/qml/qmlplayerproxy.h
@@ -7,115 +7,36 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
     Q_PROPERTY(bool isLoaded READ isLoaded NOTIFY trackChanged)
-    Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
-    Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
-    Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
-    Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist
-                    NOTIFY albumArtistChanged)
-    Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
-    Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged)
-    Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged)
-    Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged)
-    Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber
-                    NOTIFY trackNumberChanged)
-    Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged)
-    Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged)
-    Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged)
-    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
-    Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged)
-    Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged)
     QML_NAMED_ELEMENT(Player)
     QML_UNCREATABLE("Only accessible via Mixxx.PlayerManager.getPlayer(group)")
 
-    Q_PROPERTY(int waveformLength READ getWaveformLength NOTIFY waveformLengthChanged)
-    Q_PROPERTY(QString waveformTexture READ getWaveformTexture NOTIFY waveformTextureChanged)
-    Q_PROPERTY(int waveformTextureSize READ getWaveformTextureSize NOTIFY
-                    waveformTextureSizeChanged)
-    Q_PROPERTY(int waveformTextureStride READ getWaveformTextureStride NOTIFY
-                    waveformTextureStrideChanged)
-
-    Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT);
-    Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT);
-#ifdef __STEM__
-    Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT);
-#endif
-
   public:
     explicit QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent = nullptr);
 
     bool isLoaded() const;
-    QString getTrack() const;
-    QString getTitle() const;
-    QString getArtist() const;
-    QString getAlbum() const;
-    QString getAlbumArtist() const;
-    QString getGenre() const;
-    QString getComposer() const;
-    QString getGrouping() const;
-    QString getYear() const;
-    QString getTrackNumber() const;
-    QString getTrackTotal() const;
-    QString getComment() const;
-    QString getKeyText() const;
-    QColor getColor() const;
-    QUrl getCoverArtUrl() const;
-    QUrl getTrackLocationUrl() const;
-
-    int getWaveformLength() const;
-    QString getWaveformTexture() const;
-    int getWaveformTextureSize() const;
-    int getWaveformTextureStride() const;
-
     /// Needed for interacting with the raw track player object.
     BaseTrackPlayer* internalTrackPlayer() const {
         return m_pTrackPlayer;
     }
 
+    Q_INVOKABLE void loadTrack(mixxx::qml::QmlTrackProxy* track, bool play = false);
     Q_INVOKABLE void loadTrackFromLocation(const QString& trackLocation, bool play = false);
     Q_INVOKABLE void loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool play = false);
 
-#ifdef __STEM__
-    QmlStemsModel* getStemsModel() const {
-        return m_pStemsModel.get();
-    }
-#endif
-
   public slots:
     void slotTrackLoaded(TrackPointer pTrack);
+    void slotTrackUnloaded(TrackPointer pOldTrack);
     void slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack);
-    void slotTrackChanged();
-    void slotWaveformChanged();
-    void slotBeatsChanged();
-    void slotHotcuesChanged();
-#ifdef __STEM__
-    void slotStemsChanged();
-#endif
-
-    void setArtist(const QString& artist);
-    void setTitle(const QString& title);
-    void setAlbum(const QString& album);
-    void setAlbumArtist(const QString& albumArtist);
-    void setComposer(const QString& composer);
-    void setGrouping(const QString& grouping);
-    void setYear(const QString& year);
-    void setTrackNumber(const QString& trackNumber);
-    void setTrackTotal(const QString& trackTotal);
-    void setComment(const QString& comment);
-    void setKeyText(const QString& keyText);
-    void setColor(const QColor& color);
 
   signals:
     void trackLoading();
@@ -124,43 +45,18 @@ class QmlPlayerProxy : public QObject {
     void trackChanged();
     void cloneFromGroup(const QString& group);
 
-    void albumChanged();
-    void titleChanged();
-    void artistChanged();
-    void albumArtistChanged();
-    void genreChanged();
-    void composerChanged();
-    void groupingChanged();
-    void yearChanged();
-    void trackNumberChanged();
-    void trackTotalChanged();
-    void commentChanged();
-    void keyTextChanged();
-    void colorChanged();
-    void coverArtUrlChanged();
-    void trackLocationUrlChanged();
-    void cuesChanged();
+    void loadTrackFromLocationRequested(const QString& trackLocation, bool play);
+    void loadTrackRequested(TrackPointer track,
 #ifdef __STEM__
-    void stemsChanged();
+            mixxx::StemChannelSelection stemSelection,
 #endif
-
-    void loadTrackFromLocationRequested(const QString& trackLocation, bool play);
-
-    void waveformLengthChanged();
-    void waveformTextureChanged();
-    void waveformTextureSizeChanged();
-    void waveformTextureStrideChanged();
+            bool play);
 
   private:
-    std::vector<WaveformFilteredData> m_waveformData;
-    QImage m_waveformTexture;
+    QmlTrackProxy* currentTrack();
+
     QPointer<BaseTrackPlayer> m_pTrackPlayer;
     TrackPointer m_pCurrentTrack;
-    QmlBeatsModel* m_pBeatsModel;
-    QmlCuesModel* m_pHotcuesModel;
-#ifdef __STEM__
-    std::unique_ptr<QmlStemsModel> m_pStemsModel;
-#endif
 };
 
 } // namespace qml
diff --git a/src/qml/qmltrackproxy.cpp b/src/qml/qmltrackproxy.cpp
new file mode 100644
index 000000000000..61adb7af19b8
--- /dev/null
+++ b/src/qml/qmltrackproxy.cpp
@@ -0,0 +1,244 @@
+#include "qml/qmltrackproxy.h"
+
+#include <QBuffer>
+
+#include "mixer/basetrackplayer.h"
+#include "moc_qmltrackproxy.cpp"
+#include "qml/asyncimageprovider.h"
+#include "track/track.h"
+#include "util/parented_ptr.h"
+
+#define PROPERTY_IMPL_GETTER(TYPE, NAME, GETTER) \
+    TYPE QmlTrackProxy::GETTER() const {         \
+        const TrackPointer pTrack = m_pTrack;    \
+        if (pTrack == nullptr) {                 \
+            return TYPE();                       \
+        }                                        \
+        return pTrack->GETTER();                 \
+    }
+
+#define PROPERTY_IMPL(TYPE, NAME, GETTER, SETTER)   \
+    PROPERTY_IMPL_GETTER(TYPE, NAME, GETTER)        \
+    void QmlTrackProxy::SETTER(const TYPE& value) { \
+        const TrackPointer pTrack = m_pTrack;       \
+        if (pTrack != nullptr) {                    \
+            pTrack->SETTER(value);                  \
+        }                                           \
+    }
+
+namespace mixxx {
+namespace qml {
+
+QmlTrackProxy::QmlTrackProxy(TrackPointer track, QObject* parent)
+        : QObject(parent),
+          m_pTrack(track),
+          m_pBeatsModel(make_parented<QmlBeatsModel>(this)),
+          m_pHotcuesModel(make_parented<QmlCuesModel>(this))
+#ifdef __STEM__
+          ,
+          m_pStemsModel(make_parented<QmlStemsModel>(this))
+#endif
+{
+    if (m_pTrack == nullptr) {
+        return;
+    }
+    connect(m_pTrack.get(),
+            &Track::artistChanged,
+            this,
+            &QmlTrackProxy::artistChanged);
+    connect(m_pTrack.get(),
+            &Track::titleChanged,
+            this,
+            &QmlTrackProxy::titleChanged);
+    connect(m_pTrack.get(),
+            &Track::albumChanged,
+            this,
+            &QmlTrackProxy::albumChanged);
+    connect(m_pTrack.get(),
+            &Track::albumArtistChanged,
+            this,
+            &QmlTrackProxy::albumArtistChanged);
+    connect(m_pTrack.get(),
+            &Track::genreChanged,
+            this,
+            &QmlTrackProxy::genreChanged);
+    connect(m_pTrack.get(),
+            &Track::composerChanged,
+            this,
+            &QmlTrackProxy::composerChanged);
+    connect(m_pTrack.get(),
+            &Track::groupingChanged,
+            this,
+            &QmlTrackProxy::groupingChanged);
+    connect(m_pTrack.get(),
+            &Track::yearChanged,
+            this,
+            &QmlTrackProxy::yearChanged);
+    connect(m_pTrack.get(),
+            &Track::trackNumberChanged,
+            this,
+            &QmlTrackProxy::trackNumberChanged);
+    connect(m_pTrack.get(),
+            &Track::trackTotalChanged,
+            this,
+            &QmlTrackProxy::trackTotalChanged);
+    connect(m_pTrack.get(),
+            &Track::commentChanged,
+            this,
+            &QmlTrackProxy::commentChanged);
+    connect(m_pTrack.get(),
+            &Track::keyChanged,
+            this,
+            &QmlTrackProxy::keyTextChanged);
+    connect(m_pTrack.get(),
+            &Track::colorUpdated,
+            this,
+            &QmlTrackProxy::colorChanged);
+    connect(m_pTrack.get(),
+            &Track::beatsUpdated,
+            this,
+            &QmlTrackProxy::slotBeatsChanged);
+    connect(m_pTrack.get(),
+            &Track::cuesUpdated,
+            this,
+            &QmlTrackProxy::slotHotcuesChanged);
+    connect(m_pTrack.get(),
+            &Track::durationChanged,
+            this,
+            &QmlTrackProxy::durationChanged);
+#ifdef __STEM__
+    connect(m_pTrack.get(),
+            &Track::stemsUpdated,
+            this,
+            &QmlTrackProxy::slotStemsChanged);
+#endif
+    slotBeatsChanged();
+    slotHotcuesChanged();
+#ifdef __STEM__
+    slotStemsChanged();
+#endif
+}
+
+void QmlTrackProxy::slotBeatsChanged() {
+    VERIFY_OR_DEBUG_ASSERT(m_pBeatsModel) {
+        return;
+    }
+
+    const TrackPointer pTrack = m_pTrack;
+    if (pTrack) {
+        const auto trackEndPosition = mixxx::audio::FramePos{
+                pTrack->getDuration() * pTrack->getSampleRate()};
+        const auto pBeats = pTrack->getBeats();
+        m_pBeatsModel->setBeats(pBeats, trackEndPosition);
+    } else {
+        m_pBeatsModel->setBeats(nullptr, audio::kStartFramePos);
+    }
+}
+
+#ifdef __STEM__
+void QmlTrackProxy::slotStemsChanged() {
+    VERIFY_OR_DEBUG_ASSERT(m_pStemsModel) {
+        return;
+    }
+
+    if (m_pTrack) {
+        m_pStemsModel->setStems(m_pTrack->getStemInfo());
+        emit stemsChanged();
+    }
+}
+#endif
+
+void QmlTrackProxy::slotHotcuesChanged() {
+    VERIFY_OR_DEBUG_ASSERT(m_pHotcuesModel) {
+        return;
+    }
+
+    QList<CuePointer> hotcues;
+
+    if (m_pTrack) {
+        const auto& cuePoints = m_pTrack->getCuePoints();
+        for (const auto& cuePoint : cuePoints) {
+            if (cuePoint->getHotCue() == Cue::kNoHotCue) {
+                continue;
+            }
+            hotcues.append(cuePoint);
+        }
+    }
+    m_pHotcuesModel->setCues(hotcues);
+    emit cuesChanged();
+}
+
+PROPERTY_IMPL(QString, artist, getArtist, setArtist)
+PROPERTY_IMPL(QString, title, getTitle, setTitle)
+PROPERTY_IMPL(QString, album, getAlbum, setAlbum)
+PROPERTY_IMPL(QString, albumArtist, getAlbumArtist, setAlbumArtist)
+PROPERTY_IMPL_GETTER(QString, genre, getGenre)
+PROPERTY_IMPL(QString, composer, getComposer, setComposer)
+PROPERTY_IMPL(QString, grouping, getGrouping, setGrouping)
+PROPERTY_IMPL(QString, year, getYear, setYear)
+PROPERTY_IMPL(QString, trackNumber, getTrackNumber, setTrackNumber)
+PROPERTY_IMPL(QString, trackTotal, getTrackTotal, setTrackTotal)
+PROPERTY_IMPL(QString, comment, getComment, setComment)
+PROPERTY_IMPL(QString, keyText, getKeyText, setKeyText)
+
+QColor QmlTrackProxy::getColor() const {
+    if (m_pTrack == nullptr) {
+        return QColor();
+    }
+    return RgbColor::toQColor(m_pTrack->getColor());
+}
+
+double QmlTrackProxy::getDuration() const {
+    if (m_pTrack == nullptr) {
+        return -1;
+    }
+    return m_pTrack->getDuration();
+}
+
+int QmlTrackProxy::getSampleRate() const {
+    if (m_pTrack == nullptr) {
+        return 0;
+    }
+    return m_pTrack->getSampleRate();
+}
+
+void QmlTrackProxy::setColor(const QColor& value) {
+    if (m_pTrack) {
+        std::optional<RgbColor> color = RgbColor::fromQColor(value);
+        m_pTrack->setColor(color);
+    }
+}
+
+int QmlTrackProxy::getStars() const {
+    if (m_pTrack == nullptr) {
+        return -1;
+    }
+    return m_pTrack->getRating();
+}
+
+void QmlTrackProxy::setStars(int value) {
+    if (m_pTrack && value <= mixxx::TrackRecord::kMaxRating &&
+            value >= mixxx::TrackRecord::kMinRating) {
+        m_pTrack->setRating(value);
+    }
+}
+
+QUrl QmlTrackProxy::getCoverArtUrl() const {
+    if (m_pTrack == nullptr) {
+        return QUrl();
+    }
+
+    const CoverInfo coverInfo = m_pTrack->getCoverInfoWithLocation();
+    return AsyncImageProvider::trackLocationToCoverArtUrl(coverInfo.trackLocation);
+}
+
+QUrl QmlTrackProxy::getTrackLocationUrl() const {
+    if (m_pTrack == nullptr) {
+        return QUrl();
+    }
+
+    return QUrl::fromLocalFile(m_pTrack->getLocation());
+}
+
+} // namespace qml
+} // namespace mixxx
diff --git a/src/qml/qmltrackproxy.h b/src/qml/qmltrackproxy.h
new file mode 100644
index 000000000000..8b0e292d355c
--- /dev/null
+++ b/src/qml/qmltrackproxy.h
@@ -0,0 +1,148 @@
+#pragma once
+#include <QColor>
+#include <QObject>
+#include <QPointer>
+#include <QQmlEngine>
+#include <QString>
+#include <QUrl>
+#include <QtGlobal>
+
+#include "mixer/basetrackplayer.h"
+#include "qml/qmlbeatsmodel.h"
+#include "qml/qmlcuesmodel.h"
+#include "qml/qmlstemsmodel.h"
+#include "track/track_decl.h"
+#include "util/parented_ptr.h"
+
+namespace mixxx {
+namespace qml {
+
+class QmlTrackProxy : public QObject {
+    Q_OBJECT
+
+    Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
+    Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
+    Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
+    Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist
+                    NOTIFY albumArtistChanged)
+    Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
+    Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged)
+    Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged)
+    Q_PROPERTY(int stars READ getStars WRITE setStars NOTIFY starsChanged)
+    Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged)
+    Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber
+                    NOTIFY trackNumberChanged)
+    Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged)
+    Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged)
+    Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged)
+    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
+    Q_PROPERTY(double duration READ getDuration NOTIFY durationChanged)
+    Q_PROPERTY(int sampleRate READ getSampleRate NOTIFY sampleRateChanged)
+    Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged)
+    Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged)
+
+    Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel READ getBeatsModel CONSTANT);
+    Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel READ getCuesModel CONSTANT);
+#ifdef __STEM__
+    Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT);
+#endif
+
+    QML_NAMED_ELEMENT(Track)
+    QML_UNCREATABLE("Only accessible via Mixxx.PlayerManager and Mixxx.Library")
+  public:
+    explicit QmlTrackProxy(TrackPointer track, QObject* parent = nullptr);
+
+    QString getTrack() const;
+    QString getTitle() const;
+    QString getArtist() const;
+    QString getAlbum() const;
+    QString getAlbumArtist() const;
+    QString getGenre() const;
+    QString getComposer() const;
+    QString getGrouping() const;
+    QString getYear() const;
+    int getStars() const;
+    QString getTrackNumber() const;
+    QString getTrackTotal() const;
+    QString getComment() const;
+    QString getKeyText() const;
+    QColor getColor() const;
+    double getDuration() const;
+    int getSampleRate() const;
+    QUrl getCoverArtUrl() const;
+    QUrl getTrackLocationUrl() const;
+
+    QmlBeatsModel* getBeatsModel() const {
+        return m_pBeatsModel.get();
+    }
+
+    QmlCuesModel* getCuesModel() const {
+        return m_pHotcuesModel.get();
+    }
+
+#ifdef __STEM__
+    QmlStemsModel* getStemsModel() const {
+        return m_pStemsModel.get();
+    }
+#endif
+
+    TrackPointer internal() const {
+        return m_pTrack;
+    }
+
+  public slots:
+    void slotBeatsChanged();
+    void slotHotcuesChanged();
+#ifdef __STEM__
+    void slotStemsChanged();
+#endif
+
+    void setArtist(const QString& artist);
+    void setTitle(const QString& title);
+    void setAlbum(const QString& album);
+    void setAlbumArtist(const QString& albumArtist);
+    void setComposer(const QString& composer);
+    void setGrouping(const QString& grouping);
+    void setStars(int stars);
+    void setYear(const QString& year);
+    void setTrackNumber(const QString& trackNumber);
+    void setTrackTotal(const QString& trackTotal);
+    void setComment(const QString& comment);
+    void setKeyText(const QString& keyText);
+    void setColor(const QColor& color);
+
+  signals:
+    void albumChanged();
+    void titleChanged();
+    void artistChanged();
+    void albumArtistChanged();
+    void genreChanged();
+    void composerChanged();
+    void groupingChanged();
+    void starsChanged();
+    void yearChanged();
+    void trackNumberChanged();
+    void trackTotalChanged();
+    void commentChanged();
+    void keyTextChanged();
+    void colorChanged();
+    void durationChanged();
+    void sampleRateChanged();
+    void coverArtUrlChanged();
+    void trackLocationUrlChanged();
+    void cuesChanged();
+#ifdef __STEM__
+    void stemsChanged();
+#endif
+
+  private:
+    TrackPointer m_pTrack;
+    parented_ptr<QmlBeatsModel> m_pBeatsModel;
+    parented_ptr<QmlCuesModel> m_pHotcuesModel;
+#ifdef __STEM__
+    parented_ptr<QmlStemsModel> m_pStemsModel;
+#endif
+};
+
+} // namespace qml
+} // namespace mixxx
diff --git a/src/qml/qmlwaveformoverview.cpp b/src/qml/qmlwaveformoverview.cpp
index 54d2d1051ca2..133f627ed637 100644
--- a/src/qml/qmlwaveformoverview.cpp
+++ b/src/qml/qmlwaveformoverview.cpp
@@ -1,7 +1,9 @@
 #include "qml/qmlwaveformoverview.h"
 
-#include "mixer/basetrackplayer.h"
 #include "moc_qmlwaveformoverview.cpp"
+#include "qmlplayerproxy.h"
+#include "qmltrackproxy.h"
+#include "track/track.h"
 
 namespace {
 constexpr double kDesiredChannelHeight = 255;
@@ -12,7 +14,7 @@ namespace qml {
 
 QmlWaveformOverview::QmlWaveformOverview(QQuickItem* parent)
         : QQuickPaintedItem(parent),
-          m_pPlayer(nullptr),
+          m_pTrack(nullptr),
           m_channels(ChannelFlag::BothChannels),
           m_renderer(Renderer::RGB),
           m_colorHigh(0xFF0000),
@@ -20,39 +22,28 @@ QmlWaveformOverview::QmlWaveformOverview(QQuickItem* parent)
           m_colorLow(0x0000FF) {
 }
 
-QmlPlayerProxy* QmlWaveformOverview::getPlayer() const {
-    return m_pPlayer;
+QmlTrackProxy* QmlWaveformOverview::getTrack() const {
+    return m_pTrack;
 }
 
-void QmlWaveformOverview::setPlayer(QmlPlayerProxy* pPlayer) {
-    if (m_pPlayer == pPlayer) {
+void QmlWaveformOverview::setTrack(QmlTrackProxy* pTrack) {
+    if (m_pTrack == pTrack) {
         return;
     }
 
-    if (m_pPlayer != nullptr) {
-        m_pPlayer->internalTrackPlayer()->disconnect(this);
+    if (m_pTrack != nullptr && m_pTrack->internal() != nullptr) {
+        m_pTrack->internal()->disconnect(this);
     }
 
-    m_pPlayer = pPlayer;
+    m_pTrack = pTrack;
 
-    if (m_pPlayer != nullptr) {
-        setCurrentTrack(m_pPlayer->internalTrackPlayer()->getLoadedTrack());
-        connect(m_pPlayer->internalTrackPlayer(),
-                &BaseTrackPlayer::newTrackLoaded,
-                this,
-                &QmlWaveformOverview::slotTrackLoaded);
-        connect(m_pPlayer->internalTrackPlayer(),
-                &BaseTrackPlayer::loadingTrack,
-                this,
-                &QmlWaveformOverview::slotTrackLoading);
-        connect(m_pPlayer->internalTrackPlayer(),
-                &BaseTrackPlayer::playerEmpty,
+    if (m_pTrack != nullptr && pTrack->internal() != nullptr) {
+        connect(pTrack->internal().get(),
+                &Track::waveformSummaryUpdated,
                 this,
-                &QmlWaveformOverview::slotTrackUnloaded);
+                &QmlWaveformOverview::slotWaveformUpdated);
     }
-
-    emit playerChanged();
-    update();
+    slotWaveformUpdated();
 }
 
 QmlWaveformOverview::Channels QmlWaveformOverview::getChannels() const {
@@ -68,49 +59,15 @@ void QmlWaveformOverview::setChannels(QmlWaveformOverview::Channels channels) {
     emit channelsChanged(channels);
 }
 
-void QmlWaveformOverview::slotTrackLoaded(TrackPointer pTrack) {
-    // TODO: Investigate if it's a bug that this debug assertion fails when
-    // passing tracks on the command line
-    // DEBUG_ASSERT(m_pCurrentTrack == pTrack);
-    setCurrentTrack(pTrack);
-}
-
-void QmlWaveformOverview::slotTrackLoading(TrackPointer pNewTrack, TrackPointer pOldTrack) {
-    Q_UNUSED(pOldTrack); // only used in DEBUG_ASSERT
-    DEBUG_ASSERT(m_pCurrentTrack == pOldTrack);
-    setCurrentTrack(pNewTrack);
-}
-
-void QmlWaveformOverview::slotTrackUnloaded() {
-    setCurrentTrack(nullptr);
-}
-
-void QmlWaveformOverview::setCurrentTrack(TrackPointer pTrack) {
-    // TODO: Check if this is actually possible
-    if (m_pCurrentTrack == pTrack) {
-        return;
-    }
-
-    if (m_pCurrentTrack != nullptr) {
-        disconnect(m_pCurrentTrack.get(), nullptr, this, nullptr);
-    }
-
-    m_pCurrentTrack = pTrack;
-    if (pTrack != nullptr) {
-        connect(pTrack.get(),
-                &Track::waveformSummaryUpdated,
-                this,
-                &QmlWaveformOverview::slotWaveformUpdated);
-    }
-    slotWaveformUpdated();
-}
-
 void QmlWaveformOverview::slotWaveformUpdated() {
     update();
 }
 
 void QmlWaveformOverview::paint(QPainter* pPainter) {
-    TrackPointer pTrack = m_pCurrentTrack;
+    if (!m_pTrack) {
+        return;
+    }
+    TrackPointer pTrack = m_pTrack->internal();
     if (!pTrack) {
         return;
     }
diff --git a/src/qml/qmlwaveformoverview.h b/src/qml/qmlwaveformoverview.h
index 4b51bb83747d..1b3ebe745095 100644
--- a/src/qml/qmlwaveformoverview.h
+++ b/src/qml/qmlwaveformoverview.h
@@ -6,18 +6,17 @@
 #include <QQuickItem>
 #include <QQuickPaintedItem>
 
-#include "qml/qmlplayerproxy.h"
-#include "track/track.h"
+#include "qmlplayerproxy.h"
+#include "waveform/waveform.h"
 
 namespace mixxx {
 namespace qml {
 
-
 class QmlWaveformOverview : public QQuickPaintedItem {
     Q_OBJECT
     Q_FLAGS(Channels)
-    Q_PROPERTY(mixxx::qml::QmlPlayerProxy* player READ getPlayer WRITE setPlayer
-                    NOTIFY playerChanged REQUIRED)
+    Q_PROPERTY(mixxx::qml::QmlTrackProxy* track READ getTrack WRITE setTrack
+                    NOTIFY trackChanged REQUIRED)
     Q_PROPERTY(Channels channels READ getChannels WRITE setChannels NOTIFY channelsChanged)
     Q_PROPERTY(Renderer renderer MEMBER m_renderer NOTIFY rendererChanged)
     Q_PROPERTY(QColor colorHigh MEMBER m_colorHigh NOTIFY colorHighChanged)
@@ -44,19 +43,16 @@ class QmlWaveformOverview : public QQuickPaintedItem {
 
     void paint(QPainter* painter) override;
 
-    void setPlayer(QmlPlayerProxy* player);
-    QmlPlayerProxy* getPlayer() const;
+    void setTrack(QmlTrackProxy* track);
+    QmlTrackProxy* getTrack() const;
 
     void setChannels(Channels channels);
     Channels getChannels() const;
   private slots:
-    void slotTrackLoaded(TrackPointer pLoadedTrack);
-    void slotTrackLoading(TrackPointer pNewTrack, TrackPointer pOldTrack);
-    void slotTrackUnloaded();
     void slotWaveformUpdated();
 
   signals:
-    void playerChanged();
+    void trackChanged();
     void channelsChanged(mixxx::qml::QmlWaveformOverview::Channels channels);
 #if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
     void rendererChanged(Renderer renderer);
@@ -68,7 +64,6 @@ class QmlWaveformOverview : public QQuickPaintedItem {
     void colorLowChanged(const QColor& color);
 
   private:
-    void setCurrentTrack(TrackPointer pTrack);
     void drawFiltered(QPainter* pPainter,
             Channels channels,
             ConstWaveformPointer pWaveform,
@@ -78,9 +73,7 @@ class QmlWaveformOverview : public QQuickPaintedItem {
             ConstWaveformPointer pWaveform,
             int completion) const;
     QColor getRgbPenColor(ConstWaveformPointer pWaveform, int completion) const;
-
-    QPointer<QmlPlayerProxy> m_pPlayer;
-    TrackPointer m_pCurrentTrack;
+    QmlTrackProxy* m_pTrack;
     Channels m_channels;
     Renderer m_renderer;
     QColor m_colorHigh;
diff --git a/src/test/controller_mapping_validation_test.cpp b/src/test/controller_mapping_validation_test.cpp
index 1b2d7b37d72d..2b886449412a 100644
--- a/src/test/controller_mapping_validation_test.cpp
+++ b/src/test/controller_mapping_validation_test.cpp
@@ -7,6 +7,7 @@
 
 #include "controllers/defs_controllers.h"
 #include "controllers/scripting/legacy/controllerscriptenginelegacy.h"
+#include "track/track.h"
 #ifdef MIXXX_USE_QML
 #include "effects/effectsmanager.h"
 #include "engine/channelhandle.h"


Reviews:
user: login: JoergAtGithub
body: How does this scale with thousands of tracks in a table? How many track proxies would exist in this case?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-19T20:09:55Z
user: login: ywwg
body: can you generate a code coverage report for your changes?  As part of the new QML world we should aim for very coverage, which will prevent a lot of the UI bugs we get with the current barely-tested XML system.

If we do not have a good testing framework in place, we don't have to make one for this change, but we should start thinking about how to create one.
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-05T13:25:46Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T07:17:35Z
user: login: Swiftb0y
body: couple thoughts
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T12:35:29Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T23:44:50Z
user: login: Swiftb0y
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T09:56:17Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T14:24:45Z
user: login: Swiftb0y
body: couple more comments, this is looking good
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:30:58Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T16:59:04Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T17:03:24Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T17:05:12Z
user: login: Swiftb0y
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T17:39:37Z
user: login: Swiftb0y
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T17:41:24Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T17:42:35Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T17:45:02Z
user: login: Swiftb0y
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T17:58:33Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T23:20:27Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T23:25:57Z
user: login: Swiftb0y
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T08:23:41Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T15:43:36Z
user: login: Swiftb0y
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T16:31:19Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T08:45:26Z
user: login: Swiftb0y
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T09:39:51Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T16:54:04Z
user: login: Swiftb0y
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T19:15:05Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T18:57:21Z
user: login: Swiftb0y
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T19:06:44Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T20:21:58Z
user: login: acolombier
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T23:22:14Z
user: login: Swiftb0y
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T08:28:58Z
user: login: JoergAtGithub
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T13:12:12Z
user: login: Swiftb0y
body: LGTM. @ywwg seems to be unresponsive. I'm taking the liberty to merge regardless.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T08:32:10Z

Review comments:
diff_hunk: @@ -69,323 +57,54 @@ void QmlPlayerProxy::loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool
 
 void QmlPlayerProxy::slotTrackLoaded(TrackPointer pTrack) {
     m_pCurrentTrack = pTrack;
-    if (pTrack != nullptr) {
-        connect(pTrack.get(),
-                &Track::artistChanged,
-                this,
-                &QmlPlayerProxy::artistChanged);
-        connect(pTrack.get(),
-                &Track::titleChanged,
-                this,
-                &QmlPlayerProxy::titleChanged);
-        connect(pTrack.get(),
-                &Track::albumChanged,
-                this,
-                &QmlPlayerProxy::albumChanged);
-        connect(pTrack.get(),
-                &Track::albumArtistChanged,
-                this,
-                &QmlPlayerProxy::albumArtistChanged);
-        connect(pTrack.get(),
-                &Track::genreChanged,
-                this,
-                &QmlPlayerProxy::genreChanged);
-        connect(pTrack.get(),
-                &Track::composerChanged,
-                this,
-                &QmlPlayerProxy::composerChanged);
-        connect(pTrack.get(),
-                &Track::groupingChanged,
-                this,
-                &QmlPlayerProxy::groupingChanged);
-        connect(pTrack.get(),
-                &Track::yearChanged,
-                this,
-                &QmlPlayerProxy::yearChanged);
-        connect(pTrack.get(),
-                &Track::trackNumberChanged,
-                this,
-                &QmlPlayerProxy::trackNumberChanged);
-        connect(pTrack.get(),
-                &Track::trackTotalChanged,
-                this,
-                &QmlPlayerProxy::trackTotalChanged);
-        connect(pTrack.get(),
-                &Track::commentChanged,
-                this,
-                &QmlPlayerProxy::commentChanged);
-        connect(pTrack.get(),
-                &Track::keyChanged,
-                this,
-                &QmlPlayerProxy::keyTextChanged);
-        connect(pTrack.get(),
-                &Track::colorUpdated,
-                this,
-                &QmlPlayerProxy::colorChanged);
-        connect(pTrack.get(),
-                &Track::waveformUpdated,
-                this,
-                &QmlPlayerProxy::slotWaveformChanged);
-        connect(pTrack.get(),
-                &Track::beatsUpdated,
-                this,
-                &QmlPlayerProxy::slotBeatsChanged);
-        connect(pTrack.get(),
-                &Track::cuesUpdated,
-                this,
-                &QmlPlayerProxy::slotHotcuesChanged);
-#ifdef __STEM__
-        connect(pTrack.get(),
-                &Track::stemsUpdated,
-                this,
-                &QmlPlayerProxy::slotStemsChanged);
-#endif
-        slotBeatsChanged();
-        slotHotcuesChanged();
-#ifdef __STEM__
-        slotStemsChanged();
-#endif
-        slotWaveformChanged();
-    }
     emit trackChanged();
     emit trackLoaded();
 }
 
-void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) {
-    VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) {
-        qWarning() << "QML Player proxy was expected to contain "
-                   << pOldTrack.get() << "as active track but got"
-                   << m_pCurrentTrack.get();
-    }
-
-    if (pNewTrack.get() == m_pCurrentTrack.get()) {
-        emit trackLoading();
-        return;
-    }
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack != nullptr) {
-        disconnect(pTrack.get(), nullptr, this, nullptr);
+void QmlPlayerProxy::slotTrackUnloaded(TrackPointer pOldTrack) {
+    // VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) {
+    //     qWarning() << "QML Player proxy was expected to contain "
+    //                << pOldTrack.get() << "as active track but got"
+    //                << m_pCurrentTrack.get();
+    // }
+    if (m_pCurrentTrack != nullptr) {
+        disconnect(m_pCurrentTrack.get(), nullptr, this, nullptr);
     }
     m_pCurrentTrack.reset();
-    m_pCurrentTrack = pNewTrack;
-    m_waveformTexture = QImage();
     emit trackChanged();
-    emit trackLoading();
+    emit trackUnloaded();
 }
 
-void QmlPlayerProxy::slotTrackChanged() {
-    emit artistChanged();
-    emit titleChanged();
-    emit albumChanged();
-    emit albumArtistChanged();
-    emit genreChanged();
-    emit composerChanged();
-    emit groupingChanged();
-    emit yearChanged();
-    emit trackNumberChanged();
-    emit trackTotalChanged();
-    emit commentChanged();
-    emit keyTextChanged();
-    emit colorChanged();
-    emit coverArtUrlChanged();
-    emit trackLocationUrlChanged();
-#ifdef __STEM__
-    emit stemsChanged();
-#endif
-
-    emit waveformLengthChanged();
-    emit waveformTextureChanged();
-    emit waveformTextureSizeChanged();
-    emit waveformTextureStrideChanged();
+QmlTrackProxy* QmlPlayerProxy::currentTrack() {
+    auto* pTrack = new QmlTrackProxy(m_pCurrentTrack, this);
+    QQmlEngine::setObjectOwnership(pTrack, QQmlEngine::JavaScriptOwnership);
+    return pTrack;
 }
 
-void QmlPlayerProxy::slotWaveformChanged() {
-    emit waveformLengthChanged();
-    emit waveformTextureSizeChanged();
-    emit waveformTextureStrideChanged();
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (!pTrack) {
-        return;
-    }
-    const ConstWaveformPointer pWaveform =
-            pTrack->getWaveform();
-    if (!pWaveform) {
-        return;
-    }
-    const int textureWidth = pWaveform->getTextureStride();
-    const int textureHeight = pWaveform->getTextureSize() / pWaveform->getTextureStride();
-
-    const WaveformData* data = pWaveform->data();
-    // Make a copy of the waveform data, stripping the stems portion. Note that the datasize is
-    // different from the texture size -- we want the full texture size so the upload works. See
-    // m_data in waveform/waveform.h.
-    m_waveformData.resize(pWaveform->getTextureSize());
-    for (int i = 0; i < pWaveform->getDataSize(); i++) {
-        m_waveformData[i] = data[i].filtered;
-    }
-
-    m_waveformTexture =
-            QImage(reinterpret_cast<const uchar*>(m_waveformData.data()),
-                    textureWidth,
-                    textureHeight,
-                    QImage::Format_RGBA8888);
-    DEBUG_ASSERT(!m_waveformTexture.isNull());
-    emit waveformTextureChanged();
-}
-
-void QmlPlayerProxy::slotBeatsChanged() {
-    VERIFY_OR_DEBUG_ASSERT(m_pBeatsModel != nullptr) {
-        return;
-    }
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        const auto trackEndPosition = mixxx::audio::FramePos{
-                pTrack->getDuration() * pTrack->getSampleRate()};
-        const auto pBeats = pTrack->getBeats();
-        m_pBeatsModel->setBeats(pBeats, trackEndPosition);
-    } else {
-        m_pBeatsModel->setBeats(nullptr, audio::kStartFramePos);
-    }
-}
-
-#ifdef __STEM__
-void QmlPlayerProxy::slotStemsChanged() {
-    VERIFY_OR_DEBUG_ASSERT(m_pStemsModel != nullptr) {
-        return;
-    }
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        m_pStemsModel->setStems(pTrack->getStemInfo());
-        emit stemsChanged();
-    }
-}
-#endif
+void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) {
+    // VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) {
+    //     qWarning() << "QML Player proxy was expected to contain "
+    //                << pOldTrack.get() << "as active track but got"
+    //                << m_pCurrentTrack.get();
+    // }
path: src/qml/qmlplayerproxy.cpp
user: login: JoergAtGithub
body: Why is this commented out? Either uncomment it, or delete it!
created_at: 2025-04-19T19:57:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,143 @@
+#pragma once
+#include <qglobal.h>
+#include <qobject.h>
+
+#include <QColor>
+#include <QObject>
+#include <QPointer>
+#include <QQmlEngine>
+#include <QString>
+#include <QUrl>
+
+#include "mixer/basetrackplayer.h"
+#include "qml/qmlbeatsmodel.h"
+#include "qml/qmlcuesmodel.h"
+#include "qml/qmlstemsmodel.h"
+#include "track/track_decl.h"
+
+namespace mixxx {
+namespace qml {
+
+class QmlTrackProxy : public QObject {
+    Q_OBJECT
+
+    Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
+    Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
+    Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
+    Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist
+                    NOTIFY albumArtistChanged)
+    Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
+    Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged)
+    Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged)
+    Q_PROPERTY(int stars READ getStars WRITE setStars NOTIFY starsChanged)
+    Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged)
+    Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber
+                    NOTIFY trackNumberChanged)
+    Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged)
+    Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged)
+    Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged)
+    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
+    Q_PROPERTY(double duration READ getDuration NOTIFY durationChanged)
+    Q_PROPERTY(int sampleRate READ getSampleRate NOTIFY sampleRateChanged)
+    Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged)
+    Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged)
+
+    Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT);
+    Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT);
+#ifdef __STEM__
+    Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT);
+#endif
+
+    QML_NAMED_ELEMENT(Track)
+    QML_UNCREATABLE("Only accessible via Mixxx.PlayerManager and Mixxx.Library")
+  public:
+    explicit QmlTrackProxy(TrackPointer track, QObject* parent = nullptr);
+
+    QString getTrack() const;
+    QString getTitle() const;
+    QString getArtist() const;
+    QString getAlbum() const;
+    QString getAlbumArtist() const;
+    QString getGenre() const;
+    QString getComposer() const;
+    QString getGrouping() const;
+    QString getYear() const;
+    int getStars() const;
+    QString getTrackNumber() const;
+    QString getTrackTotal() const;
+    QString getComment() const;
+    QString getKeyText() const;
+    QColor getColor() const;
+    double getDuration() const;
+    int getSampleRate() const;
+    QUrl getCoverArtUrl() const;
+    QUrl getTrackLocationUrl() const;
+
+#ifdef __STEM__
+    QmlStemsModel* getStemsModel() const {
+        return m_pStemsModel.get();
+    }
+#endif
+
+    TrackPointer internal() const {
+        return m_pTrack;
+    }
+
+  public slots:
+    void slotBeatsChanged();
+    void slotHotcuesChanged();
+#ifdef __STEM__
+    void slotStemsChanged();
+#endif
+
+    void setArtist(const QString& artist);
+    void setTitle(const QString& title);
+    void setAlbum(const QString& album);
+    void setAlbumArtist(const QString& albumArtist);
+    void setComposer(const QString& composer);
+    void setGrouping(const QString& grouping);
+    void setStars(int stars);
+    void setYear(const QString& year);
+    void setTrackNumber(const QString& trackNumber);
+    void setTrackTotal(const QString& trackTotal);
+    void setComment(const QString& comment);
+    void setKeyText(const QString& keyText);
+    void setColor(const QColor& color);
+
+  signals:
+    void albumChanged();
+    void titleChanged();
+    void artistChanged();
+    void albumArtistChanged();
+    void genreChanged();
+    void composerChanged();
+    void groupingChanged();
+    void starsChanged();
+    void yearChanged();
+    void trackNumberChanged();
+    void trackTotalChanged();
+    void commentChanged();
+    void keyTextChanged();
+    void colorChanged();
+    void durationChanged();
+    void sampleRateChanged();
+    void coverArtUrlChanged();
+    void trackLocationUrlChanged();
+    void cuesChanged();
+#ifdef __STEM__
+    void stemsChanged();
+#endif
+
+  private:
+    TrackPointer m_pTrack;
+    QmlBeatsModel* m_pBeatsModel;
+    QmlCuesModel* m_pHotcuesModel;
+#ifdef __STEM__
+    std::unique_ptr<QmlStemsModel> m_pStemsModel;
+#endif
+};
+
+} // namespace qml
+} // namespace mixxx
+
+// Q_DECLARE_METATYPE(mixxx::qml::QmlTrackProxy*)
path: src/qml/qmltrackproxy.h
user: login: JoergAtGithub
body: Why is this commented?
created_at: 2025-04-19T20:02:36Z
author_association: MEMBER
diff_hunk: @@ -31,6 +32,20 @@ QmlPlayerProxy* QmlPlayerManagerProxy::getPlayer(const QString& group) {
             [this, group](const QString& trackLocation, bool play) {
                 loadLocationToPlayer(trackLocation, group, play);
             });
+    connect(pPlayerProxy,
+            &QmlPlayerProxy::loadTrackRequested,
+            this,
+            [this, group](TrackPointer track,
+#ifdef __STEM__
path: src/qml/qmlplayermanagerproxy.cpp
user: login: ywwg
body: why are we ifdeffing stems? Can we assume that by the time QML exists, every build will have stems?  if so, then I'm ok making stems a prerequisite for building qml.  (but not if this would force *everyone* to have stems turned on)
created_at: 2025-05-05T13:23:07Z
author_association: MEMBER
diff_hunk: @@ -31,6 +32,20 @@ QmlPlayerProxy* QmlPlayerManagerProxy::getPlayer(const QString& group) {
             [this, group](const QString& trackLocation, bool play) {
                 loadLocationToPlayer(trackLocation, group, play);
             });
+    connect(pPlayerProxy,
+            &QmlPlayerProxy::loadTrackRequested,
+            this,
+            [this, group](TrackPointer track,
+#ifdef __STEM__
path: src/qml/qmlplayermanagerproxy.cpp
user: login: acolombier
body: QML is already expected to be enabled for controller screens rendering, since 2.6. We could expect that both `QML` and `STEM` might be stable enough when we reach 3.0 to remove these two flags tho, but for now they are needed. 
created_at: 2025-05-06T07:17:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,143 @@
+#pragma once
+#include <qglobal.h>
+#include <qobject.h>
+
+#include <QColor>
+#include <QObject>
+#include <QPointer>
+#include <QQmlEngine>
+#include <QString>
+#include <QUrl>
+
+#include "mixer/basetrackplayer.h"
+#include "qml/qmlbeatsmodel.h"
+#include "qml/qmlcuesmodel.h"
+#include "qml/qmlstemsmodel.h"
+#include "track/track_decl.h"
+
+namespace mixxx {
+namespace qml {
+
+class QmlTrackProxy : public QObject {
+    Q_OBJECT
+
+    Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
+    Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
+    Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
+    Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist
+                    NOTIFY albumArtistChanged)
+    Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
+    Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged)
+    Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged)
+    Q_PROPERTY(int stars READ getStars WRITE setStars NOTIFY starsChanged)
+    Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged)
+    Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber
+                    NOTIFY trackNumberChanged)
+    Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged)
+    Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged)
+    Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged)
+    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
+    Q_PROPERTY(double duration READ getDuration NOTIFY durationChanged)
+    Q_PROPERTY(int sampleRate READ getSampleRate NOTIFY sampleRateChanged)
+    Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged)
+    Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged)
+
+    Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT);
+    Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT);
+#ifdef __STEM__
+    Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT);
path: src/qml/qmltrackproxy.h
user: login: Swiftb0y
body: Do you know how the definition of CONSTANT relates to pointers?

> The presence of the CONSTANT attribute indicates that the property value is constant. For a given object instance, the READ method of a constant property must return the same value every time it is called. This constant value may be different for different instances of the object. A constant property cannot have a WRITE method or a NOTIFY signal.

Does this only say the pointer stays the same or the underlying model needs to stay the same? Can we guarantee the pointer to stay the same?
created_at: 2025-05-06T12:27:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,143 @@
+#pragma once
+#include <qglobal.h>
+#include <qobject.h>
path: src/qml/qmltrackproxy.h
user: login: Swiftb0y
body: bogus includes
created_at: 2025-05-06T12:27:25Z
author_association: MEMBER
diff_hunk: @@ -69,323 +57,54 @@ void QmlPlayerProxy::loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool
 
 void QmlPlayerProxy::slotTrackLoaded(TrackPointer pTrack) {
     m_pCurrentTrack = pTrack;
-    if (pTrack != nullptr) {
-        connect(pTrack.get(),
-                &Track::artistChanged,
-                this,
-                &QmlPlayerProxy::artistChanged);
-        connect(pTrack.get(),
-                &Track::titleChanged,
-                this,
-                &QmlPlayerProxy::titleChanged);
-        connect(pTrack.get(),
-                &Track::albumChanged,
-                this,
-                &QmlPlayerProxy::albumChanged);
-        connect(pTrack.get(),
-                &Track::albumArtistChanged,
-                this,
-                &QmlPlayerProxy::albumArtistChanged);
-        connect(pTrack.get(),
-                &Track::genreChanged,
-                this,
-                &QmlPlayerProxy::genreChanged);
-        connect(pTrack.get(),
-                &Track::composerChanged,
-                this,
-                &QmlPlayerProxy::composerChanged);
-        connect(pTrack.get(),
-                &Track::groupingChanged,
-                this,
-                &QmlPlayerProxy::groupingChanged);
-        connect(pTrack.get(),
-                &Track::yearChanged,
-                this,
-                &QmlPlayerProxy::yearChanged);
-        connect(pTrack.get(),
-                &Track::trackNumberChanged,
-                this,
-                &QmlPlayerProxy::trackNumberChanged);
-        connect(pTrack.get(),
-                &Track::trackTotalChanged,
-                this,
-                &QmlPlayerProxy::trackTotalChanged);
-        connect(pTrack.get(),
-                &Track::commentChanged,
-                this,
-                &QmlPlayerProxy::commentChanged);
-        connect(pTrack.get(),
-                &Track::keyChanged,
-                this,
-                &QmlPlayerProxy::keyTextChanged);
-        connect(pTrack.get(),
-                &Track::colorUpdated,
-                this,
-                &QmlPlayerProxy::colorChanged);
-        connect(pTrack.get(),
-                &Track::waveformUpdated,
-                this,
-                &QmlPlayerProxy::slotWaveformChanged);
-        connect(pTrack.get(),
-                &Track::beatsUpdated,
-                this,
-                &QmlPlayerProxy::slotBeatsChanged);
-        connect(pTrack.get(),
-                &Track::cuesUpdated,
-                this,
-                &QmlPlayerProxy::slotHotcuesChanged);
-#ifdef __STEM__
-        connect(pTrack.get(),
-                &Track::stemsUpdated,
-                this,
-                &QmlPlayerProxy::slotStemsChanged);
-#endif
-        slotBeatsChanged();
-        slotHotcuesChanged();
-#ifdef __STEM__
-        slotStemsChanged();
-#endif
-        slotWaveformChanged();
-    }
     emit trackChanged();
     emit trackLoaded();
 }
 
-void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) {
-    VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) {
-        qWarning() << "QML Player proxy was expected to contain "
-                   << pOldTrack.get() << "as active track but got"
-                   << m_pCurrentTrack.get();
-    }
-
-    if (pNewTrack.get() == m_pCurrentTrack.get()) {
-        emit trackLoading();
-        return;
-    }
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack != nullptr) {
-        disconnect(pTrack.get(), nullptr, this, nullptr);
+void QmlPlayerProxy::slotTrackUnloaded(TrackPointer pOldTrack) {
+    // VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) {
+    //     qWarning() << "QML Player proxy was expected to contain "
+    //                << pOldTrack.get() << "as active track but got"
+    //                << m_pCurrentTrack.get();
+    // }
+    if (m_pCurrentTrack != nullptr) {
+        disconnect(m_pCurrentTrack.get(), nullptr, this, nullptr);
     }
     m_pCurrentTrack.reset();
-    m_pCurrentTrack = pNewTrack;
-    m_waveformTexture = QImage();
     emit trackChanged();
-    emit trackLoading();
+    emit trackUnloaded();
 }
 
-void QmlPlayerProxy::slotTrackChanged() {
-    emit artistChanged();
-    emit titleChanged();
-    emit albumChanged();
-    emit albumArtistChanged();
-    emit genreChanged();
-    emit composerChanged();
-    emit groupingChanged();
-    emit yearChanged();
-    emit trackNumberChanged();
-    emit trackTotalChanged();
-    emit commentChanged();
-    emit keyTextChanged();
-    emit colorChanged();
-    emit coverArtUrlChanged();
-    emit trackLocationUrlChanged();
-#ifdef __STEM__
-    emit stemsChanged();
-#endif
-
-    emit waveformLengthChanged();
-    emit waveformTextureChanged();
-    emit waveformTextureSizeChanged();
-    emit waveformTextureStrideChanged();
+QmlTrackProxy* QmlPlayerProxy::currentTrack() {
+    auto* pTrack = new QmlTrackProxy(m_pCurrentTrack, this);
+    QQmlEngine::setObjectOwnership(pTrack, QQmlEngine::JavaScriptOwnership);
+    return pTrack;
 }
 
-void QmlPlayerProxy::slotWaveformChanged() {
-    emit waveformLengthChanged();
-    emit waveformTextureSizeChanged();
-    emit waveformTextureStrideChanged();
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (!pTrack) {
-        return;
-    }
-    const ConstWaveformPointer pWaveform =
-            pTrack->getWaveform();
-    if (!pWaveform) {
-        return;
-    }
-    const int textureWidth = pWaveform->getTextureStride();
-    const int textureHeight = pWaveform->getTextureSize() / pWaveform->getTextureStride();
-
-    const WaveformData* data = pWaveform->data();
-    // Make a copy of the waveform data, stripping the stems portion. Note that the datasize is
-    // different from the texture size -- we want the full texture size so the upload works. See
-    // m_data in waveform/waveform.h.
-    m_waveformData.resize(pWaveform->getTextureSize());
-    for (int i = 0; i < pWaveform->getDataSize(); i++) {
-        m_waveformData[i] = data[i].filtered;
-    }
-
-    m_waveformTexture =
-            QImage(reinterpret_cast<const uchar*>(m_waveformData.data()),
-                    textureWidth,
-                    textureHeight,
-                    QImage::Format_RGBA8888);
-    DEBUG_ASSERT(!m_waveformTexture.isNull());
-    emit waveformTextureChanged();
-}
-
-void QmlPlayerProxy::slotBeatsChanged() {
-    VERIFY_OR_DEBUG_ASSERT(m_pBeatsModel != nullptr) {
-        return;
-    }
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        const auto trackEndPosition = mixxx::audio::FramePos{
-                pTrack->getDuration() * pTrack->getSampleRate()};
-        const auto pBeats = pTrack->getBeats();
-        m_pBeatsModel->setBeats(pBeats, trackEndPosition);
-    } else {
-        m_pBeatsModel->setBeats(nullptr, audio::kStartFramePos);
-    }
-}
-
-#ifdef __STEM__
-void QmlPlayerProxy::slotStemsChanged() {
-    VERIFY_OR_DEBUG_ASSERT(m_pStemsModel != nullptr) {
-        return;
-    }
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        m_pStemsModel->setStems(pTrack->getStemInfo());
-        emit stemsChanged();
-    }
-}
-#endif
+void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) {
+    // VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) {
+    //     qWarning() << "QML Player proxy was expected to contain "
+    //                << pOldTrack.get() << "as active track but got"
+    //                << m_pCurrentTrack.get();
+    // }
 
-void QmlPlayerProxy::slotHotcuesChanged() {
-    VERIFY_OR_DEBUG_ASSERT(m_pHotcuesModel != nullptr) {
+    if (pNewTrack.get() == m_pCurrentTrack.get()) {
+        emit trackLoading();
         return;
     }
 
-    QList<CuePointer> hotcues;
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        const auto& cuePoints = pTrack->getCuePoints();
-        for (const auto& cuePoint : cuePoints) {
-            if (cuePoint->getHotCue() == Cue::kNoHotCue)
-                continue;
-            hotcues.append(cuePoint);
-        }
-    }
-    m_pHotcuesModel->setCues(hotcues);
-    emit cuesChanged();
-}
-
-int QmlPlayerProxy::getWaveformLength() const {
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        const ConstWaveformPointer pWaveform = pTrack->getWaveform();
-        if (pWaveform) {
-            return pWaveform->getDataSize();
-        }
-    }
-    return 0;
-}
-
-QString QmlPlayerProxy::getWaveformTexture() const {
-    if (m_waveformTexture.isNull()) {
-        return QString();
-    }
-    QByteArray byteArray;
-    QBuffer buffer(&byteArray);
-    buffer.open(QIODevice::WriteOnly);
-    m_waveformTexture.save(&buffer, "png");
-
-    QString imageData = QString::fromLatin1(byteArray.toBase64().data());
-    if (imageData.isEmpty()) {
-        return QString();
-    }
-
-    return QStringLiteral("data:image/png;base64,") + imageData;
-}
-
-int QmlPlayerProxy::getWaveformTextureSize() const {
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        const ConstWaveformPointer pWaveform = pTrack->getWaveform();
-        if (pWaveform) {
-            return pWaveform->getTextureSize();
-        }
-    }
-    return 0;
-}
-
-int QmlPlayerProxy::getWaveformTextureStride() const {
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack) {
-        const ConstWaveformPointer pWaveform = pTrack->getWaveform();
-        if (pWaveform) {
-            return pWaveform->getTextureStride();
-        }
+    if (m_pCurrentTrack != nullptr) {
+        disconnect(m_pCurrentTrack.get(), nullptr, this, nullptr);
     }
-    return 0;
+    m_pCurrentTrack.reset();
+    m_pCurrentTrack = pNewTrack;
path: src/qml/qmlplayerproxy.cpp
user: login: Swiftb0y
body: nit
```suggestion
    m_pCurrentTrack = pNewTrack;
```
created_at: 2025-05-06T12:30:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,242 @@
+#include "qml/qmltrackproxy.h"
+
+#include <QBuffer>
+
+#include "mixer/basetrackplayer.h"
+#include "moc_qmltrackproxy.cpp"
+#include "qml/asyncimageprovider.h"
+#include "track/track.h"
+
+#define PROPERTY_IMPL_GETTER(TYPE, NAME, GETTER) \
+    TYPE QmlTrackProxy::GETTER() const {         \
+        const TrackPointer pTrack = m_pTrack;    \
+        if (pTrack == nullptr) {                 \
+            return TYPE();                       \
+        }                                        \
+        return pTrack->GETTER();                 \
+    }
+
+#define PROPERTY_IMPL(TYPE, NAME, GETTER, SETTER)   \
+    PROPERTY_IMPL_GETTER(TYPE, NAME, GETTER)        \
+    void QmlTrackProxy::SETTER(const TYPE& value) { \
+        const TrackPointer pTrack = m_pTrack;       \
+        if (pTrack != nullptr) {                    \
+            pTrack->SETTER(value);                  \
+        }                                           \
+    }
+
+namespace mixxx {
+namespace qml {
+
+QmlTrackProxy::QmlTrackProxy(TrackPointer track, QObject* parent)
+        : QObject(parent),
+          m_pTrack(track),
+          m_pBeatsModel(new QmlBeatsModel(this)),
+          m_pHotcuesModel(new QmlCuesModel(this))
+#ifdef __STEM__
+          ,
+          m_pStemsModel(std::make_unique<QmlStemsModel>(this))
path: src/qml/qmltrackproxy.cpp
user: login: Swiftb0y
body: all parented_ptr?
created_at: 2025-05-06T12:33:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,143 @@
+#pragma once
+#include <qglobal.h>
+#include <qobject.h>
+
+#include <QColor>
+#include <QObject>
+#include <QPointer>
+#include <QQmlEngine>
+#include <QString>
+#include <QUrl>
+
+#include "mixer/basetrackplayer.h"
+#include "qml/qmlbeatsmodel.h"
+#include "qml/qmlcuesmodel.h"
+#include "qml/qmlstemsmodel.h"
+#include "track/track_decl.h"
+
+namespace mixxx {
+namespace qml {
+
+class QmlTrackProxy : public QObject {
+    Q_OBJECT
+
+    Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
+    Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
+    Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
+    Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist
+                    NOTIFY albumArtistChanged)
+    Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
+    Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged)
+    Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged)
+    Q_PROPERTY(int stars READ getStars WRITE setStars NOTIFY starsChanged)
+    Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged)
+    Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber
+                    NOTIFY trackNumberChanged)
+    Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged)
+    Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged)
+    Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged)
+    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
+    Q_PROPERTY(double duration READ getDuration NOTIFY durationChanged)
+    Q_PROPERTY(int sampleRate READ getSampleRate NOTIFY sampleRateChanged)
+    Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged)
+    Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged)
+
+    Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT);
+    Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT);
+#ifdef __STEM__
+    Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT);
path: src/qml/qmltrackproxy.h
user: login: acolombier
body: That's right - I don't think it is quite possible to guarantee it stays the same. Back to when I tested the `CONSTANT` side effect, it does seems to be much disruptive effect that would arise if this value was to change, since AFAIU it only instruct QML not to create any property binding with this object.
Except if you have a suggestion here?
created_at: 2025-05-06T23:44:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,143 @@
+#pragma once
+#include <qglobal.h>
+#include <qobject.h>
+
+#include <QColor>
+#include <QObject>
+#include <QPointer>
+#include <QQmlEngine>
+#include <QString>
+#include <QUrl>
+
+#include "mixer/basetrackplayer.h"
+#include "qml/qmlbeatsmodel.h"
+#include "qml/qmlcuesmodel.h"
+#include "qml/qmlstemsmodel.h"
+#include "track/track_decl.h"
+
+namespace mixxx {
+namespace qml {
+
+class QmlTrackProxy : public QObject {
+    Q_OBJECT
+
+    Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
+    Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
+    Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
+    Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist
+                    NOTIFY albumArtistChanged)
+    Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
+    Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged)
+    Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged)
+    Q_PROPERTY(int stars READ getStars WRITE setStars NOTIFY starsChanged)
+    Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged)
+    Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber
+                    NOTIFY trackNumberChanged)
+    Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged)
+    Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged)
+    Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged)
+    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
+    Q_PROPERTY(double duration READ getDuration NOTIFY durationChanged)
+    Q_PROPERTY(int sampleRate READ getSampleRate NOTIFY sampleRateChanged)
+    Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged)
+    Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged)
+
+    Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT);
+    Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT);
+#ifdef __STEM__
+    Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT);
path: src/qml/qmltrackproxy.h
user: login: Swiftb0y
body: Right, you can't change the model entirely, but you can still mutate the model and any signals bound to properties of the model still work?
created_at: 2025-05-07T09:56:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,143 @@
+#pragma once
+#include <qglobal.h>
+#include <qobject.h>
+
+#include <QColor>
+#include <QObject>
+#include <QPointer>
+#include <QQmlEngine>
+#include <QString>
+#include <QUrl>
+
+#include "mixer/basetrackplayer.h"
+#include "qml/qmlbeatsmodel.h"
+#include "qml/qmlcuesmodel.h"
+#include "qml/qmlstemsmodel.h"
+#include "track/track_decl.h"
+
+namespace mixxx {
+namespace qml {
+
+class QmlTrackProxy : public QObject {
+    Q_OBJECT
+
+    Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
+    Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
+    Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
+    Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist
+                    NOTIFY albumArtistChanged)
+    Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
+    Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged)
+    Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged)
+    Q_PROPERTY(int stars READ getStars WRITE setStars NOTIFY starsChanged)
+    Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged)
+    Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber
+                    NOTIFY trackNumberChanged)
+    Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged)
+    Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged)
+    Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged)
+    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
+    Q_PROPERTY(double duration READ getDuration NOTIFY durationChanged)
+    Q_PROPERTY(int sampleRate READ getSampleRate NOTIFY sampleRateChanged)
+    Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged)
+    Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged)
+
+    Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT);
+    Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT);
+#ifdef __STEM__
+    Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT);
path: src/qml/qmltrackproxy.h
user: login: acolombier
body: Yes, that's exactly that! 
created_at: 2025-05-07T14:24:45Z
author_association: MEMBER
diff_hunk: @@ -1,42 +1,20 @@
 #include "qml/qmlplayerproxy.h"
 
+#include <qqmlengine.h>
+
path: src/qml/qmlplayerproxy.cpp
user: login: Swiftb0y
body: nit
created_at: 2025-05-07T15:20:55Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
     Q_PROPERTY(bool isLoaded READ isLoaded NOTIFY trackChanged)
-    Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
-    Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
-    Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
-    Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist
-                    NOTIFY albumArtistChanged)
-    Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
-    Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged)
-    Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged)
-    Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged)
-    Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber
-                    NOTIFY trackNumberChanged)
-    Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged)
-    Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged)
-    Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged)
-    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
-    Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged)
-    Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged)
     QML_NAMED_ELEMENT(Player)
     QML_UNCREATABLE("Only accessible via Mixxx.PlayerManager.getPlayer(group)")
 
-    Q_PROPERTY(int waveformLength READ getWaveformLength NOTIFY waveformLengthChanged)
-    Q_PROPERTY(QString waveformTexture READ getWaveformTexture NOTIFY waveformTextureChanged)
-    Q_PROPERTY(int waveformTextureSize READ getWaveformTextureSize NOTIFY
-                    waveformTextureSizeChanged)
-    Q_PROPERTY(int waveformTextureStride READ getWaveformTextureStride NOTIFY
-                    waveformTextureStrideChanged)
-
-    Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT);
-    Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT);
-#ifdef __STEM__
-    Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT);
-#endif
-
   public:
     explicit QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent = nullptr);
 
     bool isLoaded() const;
-    QString getTrack() const;
-    QString getTitle() const;
-    QString getArtist() const;
-    QString getAlbum() const;
-    QString getAlbumArtist() const;
-    QString getGenre() const;
-    QString getComposer() const;
-    QString getGrouping() const;
-    QString getYear() const;
-    QString getTrackNumber() const;
-    QString getTrackTotal() const;
-    QString getComment() const;
-    QString getKeyText() const;
-    QColor getColor() const;
-    QUrl getCoverArtUrl() const;
-    QUrl getTrackLocationUrl() const;
-
-    int getWaveformLength() const;
-    QString getWaveformTexture() const;
-    int getWaveformTextureSize() const;
-    int getWaveformTextureStride() const;
-
     /// Needed for interacting with the raw track player object.
     BaseTrackPlayer* internalTrackPlayer() const {
         return m_pTrackPlayer;
     }
 
+    Q_INVOKABLE void loadTrack(mixxx::qml::QmlTrackProxy* track, bool play = false);
     Q_INVOKABLE void loadTrackFromLocation(const QString& trackLocation, bool play = false);
     Q_INVOKABLE void loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool play = false);
 
-#ifdef __STEM__
-    QmlStemsModel* getStemsModel() const {
-        return m_pStemsModel.get();
-    }
-#endif
+    QmlTrackProxy* currentTrack();
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: should this be private?
created_at: 2025-05-07T15:22:41Z
author_association: MEMBER
diff_hunk: @@ -1,4 +1,7 @@
 #pragma once
+#include <qglobal.h>
+#include <qobject.h>
+
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: nit
```suggestion
```
created_at: 2025-05-07T15:23:22Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: should we also add a setter that dispatches to `loadTrack` directly for symmetry?
created_at: 2025-05-07T15:27:09Z
author_association: MEMBER
diff_hunk: @@ -69,323 +57,47 @@ void QmlPlayerProxy::loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool
 
 void QmlPlayerProxy::slotTrackLoaded(TrackPointer pTrack) {
     m_pCurrentTrack = pTrack;
-    if (pTrack != nullptr) {
-        connect(pTrack.get(),
-                &Track::artistChanged,
-                this,
-                &QmlPlayerProxy::artistChanged);
-        connect(pTrack.get(),
-                &Track::titleChanged,
-                this,
-                &QmlPlayerProxy::titleChanged);
-        connect(pTrack.get(),
-                &Track::albumChanged,
-                this,
-                &QmlPlayerProxy::albumChanged);
-        connect(pTrack.get(),
-                &Track::albumArtistChanged,
-                this,
-                &QmlPlayerProxy::albumArtistChanged);
-        connect(pTrack.get(),
-                &Track::genreChanged,
-                this,
-                &QmlPlayerProxy::genreChanged);
-        connect(pTrack.get(),
-                &Track::composerChanged,
-                this,
-                &QmlPlayerProxy::composerChanged);
-        connect(pTrack.get(),
-                &Track::groupingChanged,
-                this,
-                &QmlPlayerProxy::groupingChanged);
-        connect(pTrack.get(),
-                &Track::yearChanged,
-                this,
-                &QmlPlayerProxy::yearChanged);
-        connect(pTrack.get(),
-                &Track::trackNumberChanged,
-                this,
-                &QmlPlayerProxy::trackNumberChanged);
-        connect(pTrack.get(),
-                &Track::trackTotalChanged,
-                this,
-                &QmlPlayerProxy::trackTotalChanged);
-        connect(pTrack.get(),
-                &Track::commentChanged,
-                this,
-                &QmlPlayerProxy::commentChanged);
-        connect(pTrack.get(),
-                &Track::keyChanged,
-                this,
-                &QmlPlayerProxy::keyTextChanged);
-        connect(pTrack.get(),
-                &Track::colorUpdated,
-                this,
-                &QmlPlayerProxy::colorChanged);
-        connect(pTrack.get(),
-                &Track::waveformUpdated,
-                this,
-                &QmlPlayerProxy::slotWaveformChanged);
-        connect(pTrack.get(),
-                &Track::beatsUpdated,
-                this,
-                &QmlPlayerProxy::slotBeatsChanged);
-        connect(pTrack.get(),
-                &Track::cuesUpdated,
-                this,
-                &QmlPlayerProxy::slotHotcuesChanged);
-#ifdef __STEM__
-        connect(pTrack.get(),
-                &Track::stemsUpdated,
-                this,
-                &QmlPlayerProxy::slotStemsChanged);
-#endif
-        slotBeatsChanged();
-        slotHotcuesChanged();
-#ifdef __STEM__
-        slotStemsChanged();
-#endif
-        slotWaveformChanged();
-    }
     emit trackChanged();
     emit trackLoaded();
 }
 
-void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) {
-    VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) {
-        qWarning() << "QML Player proxy was expected to contain "
-                   << pOldTrack.get() << "as active track but got"
-                   << m_pCurrentTrack.get();
-    }
-
-    if (pNewTrack.get() == m_pCurrentTrack.get()) {
-        emit trackLoading();
-        return;
-    }
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack != nullptr) {
-        disconnect(pTrack.get(), nullptr, this, nullptr);
+void QmlPlayerProxy::slotTrackUnloaded(TrackPointer pOldTrack) {
+    // VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) {
+    //     qWarning() << "QML Player proxy was expected to contain "
+    //                << pOldTrack.get() << "as active track but got"
+    //                << m_pCurrentTrack.get();
+    // }
path: src/qml/qmlplayerproxy.cpp
user: login: Swiftb0y
body: reevaluate commented out code
created_at: 2025-05-07T15:29:32Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
     Q_PROPERTY(bool isLoaded READ isLoaded NOTIFY trackChanged)
-    Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
-    Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
-    Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
-    Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist
-                    NOTIFY albumArtistChanged)
-    Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
-    Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged)
-    Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged)
-    Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged)
-    Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber
-                    NOTIFY trackNumberChanged)
-    Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged)
-    Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged)
-    Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged)
-    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
-    Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged)
-    Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged)
     QML_NAMED_ELEMENT(Player)
     QML_UNCREATABLE("Only accessible via Mixxx.PlayerManager.getPlayer(group)")
 
-    Q_PROPERTY(int waveformLength READ getWaveformLength NOTIFY waveformLengthChanged)
-    Q_PROPERTY(QString waveformTexture READ getWaveformTexture NOTIFY waveformTextureChanged)
-    Q_PROPERTY(int waveformTextureSize READ getWaveformTextureSize NOTIFY
-                    waveformTextureSizeChanged)
-    Q_PROPERTY(int waveformTextureStride READ getWaveformTextureStride NOTIFY
-                    waveformTextureStrideChanged)
-
-    Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT);
-    Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT);
-#ifdef __STEM__
-    Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT);
-#endif
-
   public:
     explicit QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent = nullptr);
 
     bool isLoaded() const;
-    QString getTrack() const;
-    QString getTitle() const;
-    QString getArtist() const;
-    QString getAlbum() const;
-    QString getAlbumArtist() const;
-    QString getGenre() const;
-    QString getComposer() const;
-    QString getGrouping() const;
-    QString getYear() const;
-    QString getTrackNumber() const;
-    QString getTrackTotal() const;
-    QString getComment() const;
-    QString getKeyText() const;
-    QColor getColor() const;
-    QUrl getCoverArtUrl() const;
-    QUrl getTrackLocationUrl() const;
-
-    int getWaveformLength() const;
-    QString getWaveformTexture() const;
-    int getWaveformTextureSize() const;
-    int getWaveformTextureStride() const;
-
     /// Needed for interacting with the raw track player object.
     BaseTrackPlayer* internalTrackPlayer() const {
         return m_pTrackPlayer;
     }
 
+    Q_INVOKABLE void loadTrack(mixxx::qml::QmlTrackProxy* track, bool play = false);
     Q_INVOKABLE void loadTrackFromLocation(const QString& trackLocation, bool play = false);
     Q_INVOKABLE void loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool play = false);
 
-#ifdef __STEM__
-    QmlStemsModel* getStemsModel() const {
-        return m_pStemsModel.get();
-    }
-#endif
+    QmlTrackProxy* currentTrack();
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: Afaik accessors for Qt property needs to be public 
created_at: 2025-05-07T16:59:04Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: I'm not sure. The rationale for not adding it is that loading a track is a non trivial operation (due to complexity and disruptive side effects it may have), and properties in QML may easily suffer a bug due to property binding. That's why I kept the loadTrack as a method, which you must explicitly call, instead having the risk of that implicit magic side effects 
created_at: 2025-05-07T17:03:24Z
author_association: MEMBER
diff_hunk: @@ -69,323 +57,47 @@ void QmlPlayerProxy::loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool
 
 void QmlPlayerProxy::slotTrackLoaded(TrackPointer pTrack) {
     m_pCurrentTrack = pTrack;
-    if (pTrack != nullptr) {
-        connect(pTrack.get(),
-                &Track::artistChanged,
-                this,
-                &QmlPlayerProxy::artistChanged);
-        connect(pTrack.get(),
-                &Track::titleChanged,
-                this,
-                &QmlPlayerProxy::titleChanged);
-        connect(pTrack.get(),
-                &Track::albumChanged,
-                this,
-                &QmlPlayerProxy::albumChanged);
-        connect(pTrack.get(),
-                &Track::albumArtistChanged,
-                this,
-                &QmlPlayerProxy::albumArtistChanged);
-        connect(pTrack.get(),
-                &Track::genreChanged,
-                this,
-                &QmlPlayerProxy::genreChanged);
-        connect(pTrack.get(),
-                &Track::composerChanged,
-                this,
-                &QmlPlayerProxy::composerChanged);
-        connect(pTrack.get(),
-                &Track::groupingChanged,
-                this,
-                &QmlPlayerProxy::groupingChanged);
-        connect(pTrack.get(),
-                &Track::yearChanged,
-                this,
-                &QmlPlayerProxy::yearChanged);
-        connect(pTrack.get(),
-                &Track::trackNumberChanged,
-                this,
-                &QmlPlayerProxy::trackNumberChanged);
-        connect(pTrack.get(),
-                &Track::trackTotalChanged,
-                this,
-                &QmlPlayerProxy::trackTotalChanged);
-        connect(pTrack.get(),
-                &Track::commentChanged,
-                this,
-                &QmlPlayerProxy::commentChanged);
-        connect(pTrack.get(),
-                &Track::keyChanged,
-                this,
-                &QmlPlayerProxy::keyTextChanged);
-        connect(pTrack.get(),
-                &Track::colorUpdated,
-                this,
-                &QmlPlayerProxy::colorChanged);
-        connect(pTrack.get(),
-                &Track::waveformUpdated,
-                this,
-                &QmlPlayerProxy::slotWaveformChanged);
-        connect(pTrack.get(),
-                &Track::beatsUpdated,
-                this,
-                &QmlPlayerProxy::slotBeatsChanged);
-        connect(pTrack.get(),
-                &Track::cuesUpdated,
-                this,
-                &QmlPlayerProxy::slotHotcuesChanged);
-#ifdef __STEM__
-        connect(pTrack.get(),
-                &Track::stemsUpdated,
-                this,
-                &QmlPlayerProxy::slotStemsChanged);
-#endif
-        slotBeatsChanged();
-        slotHotcuesChanged();
-#ifdef __STEM__
-        slotStemsChanged();
-#endif
-        slotWaveformChanged();
-    }
     emit trackChanged();
     emit trackLoaded();
 }
 
-void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) {
-    VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) {
-        qWarning() << "QML Player proxy was expected to contain "
-                   << pOldTrack.get() << "as active track but got"
-                   << m_pCurrentTrack.get();
-    }
-
-    if (pNewTrack.get() == m_pCurrentTrack.get()) {
-        emit trackLoading();
-        return;
-    }
-
-    const TrackPointer pTrack = m_pCurrentTrack;
-    if (pTrack != nullptr) {
-        disconnect(pTrack.get(), nullptr, this, nullptr);
+void QmlPlayerProxy::slotTrackUnloaded(TrackPointer pOldTrack) {
+    // VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) {
+    //     qWarning() << "QML Player proxy was expected to contain "
+    //                << pOldTrack.get() << "as active track but got"
+    //                << m_pCurrentTrack.get();
+    // }
path: src/qml/qmlplayerproxy.cpp
user: login: acolombier
body: The reason for leaving this out is, it doesn't seem that the asserted predicate is always true. I cannot remember edge cases where this wasn't so happy to leave it in and fix forward once we stumble on them again 
created_at: 2025-05-07T17:05:12Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
     Q_PROPERTY(bool isLoaded READ isLoaded NOTIFY trackChanged)
-    Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
-    Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
-    Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
-    Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist
-                    NOTIFY albumArtistChanged)
-    Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
-    Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged)
-    Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged)
-    Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged)
-    Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber
-                    NOTIFY trackNumberChanged)
-    Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged)
-    Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged)
-    Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged)
-    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
-    Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged)
-    Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged)
     QML_NAMED_ELEMENT(Player)
     QML_UNCREATABLE("Only accessible via Mixxx.PlayerManager.getPlayer(group)")
 
-    Q_PROPERTY(int waveformLength READ getWaveformLength NOTIFY waveformLengthChanged)
-    Q_PROPERTY(QString waveformTexture READ getWaveformTexture NOTIFY waveformTextureChanged)
-    Q_PROPERTY(int waveformTextureSize READ getWaveformTextureSize NOTIFY
-                    waveformTextureSizeChanged)
-    Q_PROPERTY(int waveformTextureStride READ getWaveformTextureStride NOTIFY
-                    waveformTextureStrideChanged)
-
-    Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT);
-    Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT);
-#ifdef __STEM__
-    Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT);
-#endif
-
   public:
     explicit QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent = nullptr);
 
     bool isLoaded() const;
-    QString getTrack() const;
-    QString getTitle() const;
-    QString getArtist() const;
-    QString getAlbum() const;
-    QString getAlbumArtist() const;
-    QString getGenre() const;
-    QString getComposer() const;
-    QString getGrouping() const;
-    QString getYear() const;
-    QString getTrackNumber() const;
-    QString getTrackTotal() const;
-    QString getComment() const;
-    QString getKeyText() const;
-    QColor getColor() const;
-    QUrl getCoverArtUrl() const;
-    QUrl getTrackLocationUrl() const;
-
-    int getWaveformLength() const;
-    QString getWaveformTexture() const;
-    int getWaveformTextureSize() const;
-    int getWaveformTextureStride() const;
-
     /// Needed for interacting with the raw track player object.
     BaseTrackPlayer* internalTrackPlayer() const {
         return m_pTrackPlayer;
     }
 
+    Q_INVOKABLE void loadTrack(mixxx::qml::QmlTrackProxy* track, bool play = false);
     Q_INVOKABLE void loadTrackFromLocation(const QString& trackLocation, bool play = false);
     Q_INVOKABLE void loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool play = false);
 
-#ifdef __STEM__
-    QmlStemsModel* getStemsModel() const {
-        return m_pStemsModel.get();
-    }
-#endif
+    QmlTrackProxy* currentTrack();
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: Are you sure?
created_at: 2025-05-07T17:39:37Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: right, but doesn't the problem still apply when the function is called from for example a signal handler? IMO the QmlProxy needs to make sure to debounce these calls appropriately.
created_at: 2025-05-07T17:41:24Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
     Q_PROPERTY(bool isLoaded READ isLoaded NOTIFY trackChanged)
-    Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged)
-    Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged)
-    Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged)
-    Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist
-                    NOTIFY albumArtistChanged)
-    Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
-    Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged)
-    Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged)
-    Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged)
-    Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber
-                    NOTIFY trackNumberChanged)
-    Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged)
-    Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged)
-    Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged)
-    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
-    Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged)
-    Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged)
     QML_NAMED_ELEMENT(Player)
     QML_UNCREATABLE("Only accessible via Mixxx.PlayerManager.getPlayer(group)")
 
-    Q_PROPERTY(int waveformLength READ getWaveformLength NOTIFY waveformLengthChanged)
-    Q_PROPERTY(QString waveformTexture READ getWaveformTexture NOTIFY waveformTextureChanged)
-    Q_PROPERTY(int waveformTextureSize READ getWaveformTextureSize NOTIFY
-                    waveformTextureSizeChanged)
-    Q_PROPERTY(int waveformTextureStride READ getWaveformTextureStride NOTIFY
-                    waveformTextureStrideChanged)
-
-    Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT);
-    Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT);
-#ifdef __STEM__
-    Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT);
-#endif
-
   public:
     explicit QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent = nullptr);
 
     bool isLoaded() const;
-    QString getTrack() const;
-    QString getTitle() const;
-    QString getArtist() const;
-    QString getAlbum() const;
-    QString getAlbumArtist() const;
-    QString getGenre() const;
-    QString getComposer() const;
-    QString getGrouping() const;
-    QString getYear() const;
-    QString getTrackNumber() const;
-    QString getTrackTotal() const;
-    QString getComment() const;
-    QString getKeyText() const;
-    QColor getColor() const;
-    QUrl getCoverArtUrl() const;
-    QUrl getTrackLocationUrl() const;
-
-    int getWaveformLength() const;
-    QString getWaveformTexture() const;
-    int getWaveformTextureSize() const;
-    int getWaveformTextureStride() const;
-
     /// Needed for interacting with the raw track player object.
     BaseTrackPlayer* internalTrackPlayer() const {
         return m_pTrackPlayer;
     }
 
+    Q_INVOKABLE void loadTrack(mixxx::qml::QmlTrackProxy* track, bool play = false);
     Q_INVOKABLE void loadTrackFromLocation(const QString& trackLocation, bool play = false);
     Q_INVOKABLE void loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool play = false);
 
-#ifdef __STEM__
-    QmlStemsModel* getStemsModel() const {
-        return m_pStemsModel.get();
-    }
-#endif
+    QmlTrackProxy* currentTrack();
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: Not entirely - I'll try again when I'm back at the keyboard 
created_at: 2025-05-07T17:42:35Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: Yes exactly, the difference is that this requires an explicit call (e.g onSignal => loadTrack(...)) where if you do assignation, it could then auto load a track when the binded property changes. Perhaps we could interate on that? If we realise it makes sense to use the setter, it should be pretty trivial to add.
created_at: 2025-05-07T17:45:02Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: Right, but you can also easily bind to `onPropertyChanged` (for some `Property`) which would also get fired multiple times. So regardless of whether we have the setter or not, the `loadTrack` member function needs debouncing. wdyt? 
created_at: 2025-05-07T17:58:32Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: No because somebody binding that loadTrack inconditionnally on `onPropertyChanged` is something that can easily be spotted. I think the point I'm trying to get at is that some of this debouncing needs to happen upstream, in QML, but this is not something you can do if you have a setter that will bypass your conditional logic.
Eventually, we want to offset as m,uch logic as possible in QML, so user can easy customised behaviour without havening to fiddle with the C++ layer. 

created_at: 2025-05-07T23:20:26Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: To put it simply, with the current approach:

```
onPropertyChanged: {
    if (!deck.playing) {
        deck.loadTrack(myTrack)
    }
}
```

will load the track only if no track is playing on `deck`

```
onPropertyChanged: {
    if (!deck.playing) {
        deck.currentTrack = myTrack
    }
}
```

Will load the track only if no track is playing on `deck`, and then will load the track inconditionally, whenever `myTrack` changes. This means you now need to take care of the business logic in C++, so a user that would want to customize this needs a new Mixxx build, as opposite to be able to adjust this `if`
created_at: 2025-05-07T23:25:56Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: >  Eventually, we want to offset as m,uch logic as possible in QML, so user can easy customised behaviour without havening to fiddle with the C++ layer.

I completely agree with that in general. However, you never want to load a deck while its already playing and the check is needed for robustness. So thats why I'd prefer to have this check in C++ to ensure robustness of the user-facing API. Does that make sense?
created_at: 2025-05-08T08:23:40Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: I disagree. I don't think this kind of logic should be a hard blocker.
This also massively complexifies the error/UX handling.
The point remains anyway whether the check should be done on QML side or C++, this implicit binding as a significant implications and it makes that a method keeps us safe. If we realise it causes more problems, we simply have to add that `WRITE` annotations so this is not a fundamental architecture blocker

created_at: 2025-05-08T15:43:36Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: Its not a blocker. Lets ignore the `WRITE`/setter question completely for now. Do you agree that (accidentally) calling `loadTrack` repeatedly is an issue? If so, why does the QML need to safeguard against it? Wouldn't it make the API easier to use if the check was done on the C++ side? Why is it easier to check in QML than in C++?
created_at: 2025-05-08T16:31:19Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: > Do you agree that (accidentally) calling loadTrack repeatedly is an issue?

I agree but the call to method in QML is explicit and transparent. In orther terms, in this snippet:

```
onPropertyChanged: {
    deck.loadTrack(myTrack)
}
```

There is no risk of accidentality calling `loadTrack`. Only case where this would happen is if `property` repeatedly change, but this is easy to flag when you revie the code, and debounce if needs be (or request a different hook)

> Wouldn't it make the API easier to use if the check was done on the C++ side?

It's not because it drastically limit the customisation power that QML is meant to give us. For example, scratch DJ may want to load track on playing, this means they could easily edit the QML file like so

```
onPropertyChanged: {
    // if (!deck.playing) {
        deck.currentTrack = myTrack
    // }
}
```

Or even as custom logic for their usecase

```
onPropertyChanged: {
    if (!deck.playing || deck.currentTrack?.title?.includes("scratch"))) {
        deck.currentTrack = myTrack
    }
}
```

Making these checks on C++ side indeed fix the issue as well, but would block the customisability/creativity that QML gives you, so IMO the C++ checks should focus on critical checks only (aka ensure the stability and prevent any kind of damage such as DB or file corruption) 
created_at: 2025-05-09T08:45:26Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: Oh I think we're talking past each other. I don't mean to check `playing` but instead to check if the track is loading. That decreases the likelyhood of mixxx crashing because the binding is spamming loadtrack due to the signal being fired multiple times. I'm totally fine with the `playing` check staying in QML for now. 
created_at: 2025-05-09T09:39:51Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: Ah I see, do you mean `trackChanged` then? This signal should only fired when `m_pCurrentTrack` is changed. Or do you mean to keep track of what was the last track that had been queued for loading via `loadTrackRequested` signal?
created_at: 2025-05-09T16:54:04Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: No I meant some other signal. For instance if you had a load button, and then bound to it's `onClick: {deck.loadTrack(...)}`. If the `onClick` signal is fired multiple times in rapid succession, the expensive track loading action is executed multiple times in a row (or rather while already loading a track). That will likely cause issues.
created_at: 2025-05-09T19:15:05Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: Right, I think I understood now, so you mean we should for example discard subsequent request to load `file` if `QmlPlayerProxy` has already emitted the load signal to the player manager and is waiting for it to proceeded? 
If I got it right this time, and since this is an existing limitation, are you happy with me creating a follow issue for this?
created_at: 2025-05-12T18:57:21Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: > Right, I think I understood now, so you mean we should for example discard subsequent request to load file if QmlPlayerProxy has already emitted the load signal to the player manager and is waiting for it to proceeded?

Yes, exactly. 

>  If I got it right this time, and since this is an existing limitation

Is it? I thought the CO does have some safety checks to avoid this.
created_at: 2025-05-12T19:06:43Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: Yes, I meant it is already a problem in `QMLPlayerProxy`, and I'm only moving the code around here
created_at: 2025-05-12T20:21:58Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: acolombier
body: (see [all the 3 load operations](https://github.com/mixxxdj/mixxx/blob/4e1534c1fefd89c681259903195669f856476b36/src/qml/qmlplayermanagerproxy.cpp#L43) in `main`)
created_at: 2025-05-12T23:22:14Z
author_association: MEMBER
diff_hunk: @@ -7,115 +10,38 @@
 #include <QUrl>
 
 #include "mixer/basetrackplayer.h"
-#include "qml/qmlbeatsmodel.h"
-#include "qml/qmlcuesmodel.h"
-#include "qml/qmlstemsmodel.h"
-#include "track/cueinfo.h"
-#include "track/track.h"
-#include "waveform/waveform.h"
+#include "qmltrackproxy.h"
+#include "track/track_decl.h"
 
 namespace mixxx {
 namespace qml {
 
 class QmlPlayerProxy : public QObject {
     Q_OBJECT
+    Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged)
path: src/qml/qmlplayerproxy.h
user: login: Swiftb0y
body: Ah, I see. Yeah sure. Lets file an issue and move on.
created_at: 2025-05-13T08:28:58Z
author_association: MEMBER
diff_hunk: @@ -31,6 +32,20 @@ QmlPlayerProxy* QmlPlayerManagerProxy::getPlayer(const QString& group) {
             [this, group](const QString& trackLocation, bool play) {
                 loadLocationToPlayer(trackLocation, group, play);
             });
+    connect(pPlayerProxy,
+            &QmlPlayerProxy::loadTrackRequested,
+            this,
+            [this, group](TrackPointer track,
+#ifdef __STEM__
path: src/qml/qmlplayermanagerproxy.cpp
user: login: JoergAtGithub
body: @ywwg Do you agree with keeping these ifdefs for now?
created_at: 2025-05-18T13:12:12Z
author_association: MEMBER

PR comments:
user: login: acolombier
created_at: 2025-04-19T20:22:11Z
body: Thank you for your review!

> How does this scale with thousands of tracks in a table? How many track proxies would exist in this case?

It's working great. The reason for this is that `TableView` will only load needed rows (see [doc](https://doc.qt.io/qt-6/qml-qtquick-tableview.html#reusing-items)), so Qt will take care of loading what display on the screen (+ some extra items to smooth scrolling 
author_association: MEMBER
user: login: acolombier
created_at: 2025-05-05T15:25:48Z
body: > can you generate a code coverage report for your changes? 

Code coverage is included already (`+0.009%` on this PR). The reason there is no new code here, I'm only moving existing accessor out of the PlayerProxy into a TrackProxy. I believe the increased is related to the fact that this PR managed to reduce the code for these accessor, due to the waveform clean-up, no longer required. 

>  If we do not have a good testing framework in place, we don't have to make one for this change, but we should start thinking about how to create one.

I have [a PR that that adds UI/E2E testing for QML ](https://github.com/mixxxdj/mixxx/pull/14360). It is pretty much ready but it didn't pick much traction yet.
author_association: MEMBER
user: login: ywwg
created_at: 2025-05-05T18:27:02Z
body: > It is pretty much ready but it didn't pick much traction yet.

the PR is marked as draft, I generally don't look at PRs that are not ready for review
author_association: MEMBER
user: login: ywwg
created_at: 2025-05-05T18:28:02Z
body: (but that doesn't really make a difference, I am not qualified to review QML code anyway)
author_association: MEMBER
user: login: Swiftb0y
created_at: 2025-05-13T10:06:15Z
body: Please squash and resolve merge conflict, then we can merge.
author_association: MEMBER