diff --git a/rust/include/qobuz_backend.h b/rust/include/qobuz_backend.h index f832dd5..37a9633 100644 --- a/rust/include/qobuz_backend.h +++ b/rust/include/qobuz_backend.h @@ -37,6 +37,7 @@ enum QobuzEvent { EV_USER_OK = 23, EV_ARTIST_RELEASES_OK = 24, EV_DEEP_SHUFFLE_OK = 25, + EV_MOST_POPULAR_OK = 26, EV_GENRES_OK = 27, EV_FEATURED_ALBUMS_OK = 28, }; @@ -55,6 +56,7 @@ void qobuz_backend_get_user(QobuzBackendOpaque *backend); // Catalog void qobuz_backend_search(QobuzBackendOpaque *backend, const char *query, uint32_t offset, uint32_t limit); +void qobuz_backend_most_popular_search(QobuzBackendOpaque *backend, const char *query, uint32_t limit); void qobuz_backend_get_album(QobuzBackendOpaque *backend, const char *album_id); void qobuz_backend_get_artist(QobuzBackendOpaque *backend, int64_t artist_id); void qobuz_backend_get_playlist(QobuzBackendOpaque *backend, int64_t playlist_id, uint32_t offset, uint32_t limit); diff --git a/rust/src/api/client.rs b/rust/src/api/client.rs index de5a223..8940e38 100644 --- a/rust/src/api/client.rs +++ b/rust/src/api/client.rs @@ -453,6 +453,24 @@ impl QobuzClient { }) } + pub async fn get_most_popular( + &self, + query: &str, + offset: u32, + limit: u32, + ) -> Result { + let resp = self + .get_request("most-popular/get") + .query(&[ + ("query", query.to_string()), + ("offset", offset.to_string()), + ("limit", limit.to_string()), + ]) + .send() + .await?; + Self::check_response(resp).await + } + async fn search_tracks( &self, query: &str, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index b95a7f2..e234e2b 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -45,6 +45,7 @@ pub const EV_TRACK_URL_ERR: c_int = 18; pub const EV_GENERIC_ERR: c_int = 19; pub const EV_ARTIST_RELEASES_OK: c_int = 24; pub const EV_DEEP_SHUFFLE_OK: c_int = 25; +pub const EV_MOST_POPULAR_OK: c_int = 26; pub const EV_GENRES_OK: c_int = 27; pub const EV_FEATURED_ALBUMS_OK: c_int = 28; @@ -203,6 +204,36 @@ pub unsafe extern "C" fn qobuz_backend_search( }); } +#[no_mangle] +pub unsafe extern "C" fn qobuz_backend_most_popular_search( + ptr: *mut Backend, + query: *const c_char, + limit: u32, +) { + let inner = &(*ptr).0; + let query = CStr::from_ptr(query).to_string_lossy().into_owned(); + let client = inner.client.clone(); + let cb = inner.cb; + let ud = inner.ud; + + spawn(inner, async move { + let result = client + .lock() + .await + .get_most_popular(&query, 0, limit) + .await; + match result { + Ok(r) => call_cb( + cb, + ud, + EV_MOST_POPULAR_OK, + &serde_json::to_string(&r).unwrap_or_default(), + ), + Err(e) => call_cb(cb, ud, EV_SEARCH_ERR, &err_json(&e.to_string())), + } + }); +} + // ---------- Album ---------- #[no_mangle] diff --git a/src/backend/qobuzbackend.cpp b/src/backend/qobuzbackend.cpp index 94dd8fa..d085bb4 100644 --- a/src/backend/qobuzbackend.cpp +++ b/src/backend/qobuzbackend.cpp @@ -51,6 +51,11 @@ void QobuzBackend::search(const QString &query, quint32 offset, quint32 limit) qobuz_backend_search(m_backend, query.toUtf8().constData(), offset, limit); } +void QobuzBackend::mostPopularSearch(const QString &query, quint32 limit) +{ + qobuz_backend_most_popular_search(m_backend, query.toUtf8().constData(), limit); +} + void QobuzBackend::getAlbum(const QString &albumId) { qobuz_backend_get_album(m_backend, albumId.toUtf8().constData()); @@ -251,6 +256,9 @@ void QobuzBackend::onEvent(int eventType, const QString &json) case EV_SEARCH_OK: emit searchResult(obj); break; + case 26: // EV_MOST_POPULAR_OK + emit mostPopularResult(obj); + break; case EV_SEARCH_ERR: emit error(obj["error"].toString()); break; diff --git a/src/backend/qobuzbackend.hpp b/src/backend/qobuzbackend.hpp index 3921816..412be80 100644 --- a/src/backend/qobuzbackend.hpp +++ b/src/backend/qobuzbackend.hpp @@ -28,6 +28,7 @@ public: // --- catalog --- void search(const QString &query, quint32 offset = 0, quint32 limit = 20); + void mostPopularSearch(const QString &query, quint32 limit = 8); void getAlbum(const QString &albumId); void getArtist(qint64 artistId); void getArtistReleases(qint64 artistId, const QString &releaseType, quint32 limit = 50, quint32 offset = 0); @@ -83,6 +84,7 @@ signals: // catalog void searchResult(const QJsonObject &result); + void mostPopularResult(const QJsonObject &result); void albumLoaded(const QJsonObject &album); void artistLoaded(const QJsonObject &artist); void artistReleasesLoaded(const QString &releaseType, const QJsonArray &items, bool hasMore, int offset); diff --git a/src/view/sidepanel/view.cpp b/src/view/sidepanel/view.cpp index a7e0100..62a92e3 100644 --- a/src/view/sidepanel/view.cpp +++ b/src/view/sidepanel/view.cpp @@ -39,6 +39,14 @@ SearchTab::SearchTab(QobuzBackend *backend, PlayQueue *queue, QWidget *parent) // Result tabs m_resultTabs = new QTabWidget(this); + m_topResults = new QTreeWidget(this); + m_topResults->setHeaderLabels({tr(""), tr("Top Result"), tr("Info")}); + m_topResults->setRootIsDecorated(false); + m_topResults->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + m_topResults->header()->setSectionResizeMode(1, QHeaderView::Stretch); + m_topResults->header()->setSectionResizeMode(2, QHeaderView::Stretch); + m_topResults->header()->setStretchLastSection(false); + m_trackResults = new QTreeWidget(this); m_trackResults->setHeaderLabels({tr("Title"), tr("Artist"), tr("Album")}); m_trackResults->setRootIsDecorated(false); @@ -57,6 +65,7 @@ SearchTab::SearchTab(QobuzBackend *backend, PlayQueue *queue, QWidget *parent) m_artistResults->setHeaderLabels({tr("Artist")}); m_artistResults->setRootIsDecorated(false); + m_resultTabs->addTab(m_topResults, tr("Top Results")); m_resultTabs->addTab(m_trackResults, tr("Tracks")); m_resultTabs->addTab(m_albumResults, tr("Albums")); m_resultTabs->addTab(m_artistResults, tr("Artists")); @@ -66,7 +75,9 @@ SearchTab::SearchTab(QobuzBackend *backend, PlayQueue *queue, QWidget *parent) connect(m_searchBox, &QLineEdit::returnPressed, this, &SearchTab::onSearchSubmit); connect(m_backend, &QobuzBackend::searchResult, this, &SearchTab::onSearchResult); + connect(m_backend, &QobuzBackend::mostPopularResult, this, &SearchTab::onMostPopularResult); + connect(m_topResults, &QTreeWidget::itemDoubleClicked, this, &SearchTab::onItemDoubleClicked); connect(m_trackResults, &QTreeWidget::itemDoubleClicked, this, &SearchTab::onItemDoubleClicked); connect(m_albumResults, &QTreeWidget::itemDoubleClicked, this, &SearchTab::onItemDoubleClicked); connect(m_artistResults, &QTreeWidget::itemDoubleClicked, this, &SearchTab::onItemDoubleClicked); @@ -86,8 +97,67 @@ void SearchTab::setUserPlaylists(const QVector> &playlist void SearchTab::onSearchSubmit() { const QString q = m_searchBox->text().trimmed(); - if (!q.isEmpty()) + if (!q.isEmpty()) { + m_backend->mostPopularSearch(q, 8); m_backend->search(q, 0, 20); + m_resultTabs->setCurrentIndex(0); + } +} + +void SearchTab::onMostPopularResult(const QJsonObject &result) +{ + m_topResults->clear(); + + QFont badgeFont; + badgeFont.setBold(true); + + const QJsonArray items = result["most_popular"].toObject()["items"].toArray(); + for (const auto &value : items) { + const QJsonObject itemObj = value.toObject(); + const QString type = itemObj["type"].toString(); + const QJsonObject content = itemObj["content"].toObject(); + + auto *item = new QTreeWidgetItem(m_topResults); + item->setData(0, JsonRole, content); + + if (type == QStringLiteral("tracks")) { + const QString title = content["title"].toString(); + const QString artist = content["performer"].toObject()["name"].toString(); + const QString album = content["album"].toObject()["title"].toString(); + item->setText(0, QStringLiteral("T")); + item->setForeground(0, QColor(QStringLiteral("#2FA84F"))); + item->setFont(0, badgeFont); + item->setTextAlignment(0, Qt::AlignCenter); + item->setText(1, title); + item->setText(2, artist.isEmpty() ? album : QStringLiteral("%1 - %2").arg(artist, album)); + item->setData(0, TypeRole, QStringLiteral("track")); + item->setData(0, IdRole, static_cast(content["id"].toDouble())); + } else if (type == QStringLiteral("albums")) { + const QString title = content["title"].toString(); + const QString artist = content["artist"].toObject()["name"].toString(); + const bool hiRes = content["hires_streamable"].toBool() + || content["rights"].toObject()["hires_streamable"].toBool(); + item->setText(0, hiRes ? QStringLiteral("H") : QStringLiteral("A")); + item->setForeground(0, hiRes + ? QColor(QStringLiteral("#FFB232")) + : QColor(QStringLiteral("#8E8E93"))); + item->setFont(0, badgeFont); + item->setTextAlignment(0, Qt::AlignCenter); + item->setText(1, title); + item->setText(2, artist); + item->setData(0, TypeRole, QStringLiteral("album")); + item->setData(1, IdRole, content["id"].toString()); + } else if (type == QStringLiteral("artists")) { + item->setText(0, QStringLiteral("A")); + item->setForeground(0, QColor(QStringLiteral("#2B7CD3"))); + item->setFont(0, badgeFont); + item->setTextAlignment(0, Qt::AlignCenter); + item->setText(1, content["name"].toString()); + item->setText(2, tr("Artist")); + item->setData(0, TypeRole, QStringLiteral("artist")); + item->setData(0, IdRole, static_cast(content["id"].toDouble())); + } + } } void SearchTab::onSearchResult(const QJsonObject &result) diff --git a/src/view/sidepanel/view.hpp b/src/view/sidepanel/view.hpp index b19c8c3..600a5e3 100644 --- a/src/view/sidepanel/view.hpp +++ b/src/view/sidepanel/view.hpp @@ -30,6 +30,7 @@ namespace SidePanel private slots: void onSearchResult(const QJsonObject &result); + void onMostPopularResult(const QJsonObject &result); void onSearchSubmit(); void onItemDoubleClicked(QTreeWidgetItem *item, int column); @@ -38,6 +39,7 @@ namespace SidePanel PlayQueue *m_queue = nullptr; QLineEdit *m_searchBox = nullptr; QTabWidget *m_resultTabs = nullptr; + QTreeWidget *m_topResults = nullptr; QTreeWidget *m_trackResults = nullptr; QTreeWidget *m_albumResults = nullptr; QTreeWidget *m_artistResults = nullptr;