Initial implementation of qobuz-qt

- Rust backend (qobuz-backend static lib): Qobuz API client (reqwest/tokio),
  Symphonia audio decoder, CPAL audio output, extern "C" FFI bridge
- Qt 6 frontend mirroring spotify-qt layout: toolbar with playback controls,
  left library dock, central track list, right search panel
- Auth: email/password login with MD5-signed requests; session token persisted
  via QSettings
- Playback: double-click a track → Rust fetches stream URL → Symphonia decodes
  → CPAL outputs to default audio device
- Dark Fusion palette matching spotify-qt feel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-23 23:34:23 +01:00
commit 9402dca7ed
40 changed files with 3963 additions and 0 deletions

110
src/list/library.cpp Normal file
View File

@@ -0,0 +1,110 @@
#include "library.hpp"
#include <QHeaderView>
#include <QJsonArray>
#include <QJsonObject>
namespace List
{
static constexpr int TypeRole = Qt::UserRole + 1;
static constexpr int IdRole = Qt::UserRole + 2;
static constexpr int NameRole = Qt::UserRole + 3;
enum NodeType {
NodeFavTracks,
NodeFavAlbums,
NodeFavArtists,
NodePlaylist,
};
Library::Library(QobuzBackend *backend, QWidget *parent)
: QTreeWidget(parent)
, m_backend(backend)
{
setHeaderHidden(true);
setRootIsDecorated(true);
setContextMenuPolicy(Qt::NoContextMenu);
buildStaticNodes();
connect(m_backend, &QobuzBackend::userPlaylistsLoaded,
this, &Library::onUserPlaylistsLoaded);
connect(this, &QTreeWidget::itemClicked,
this, &Library::onItemClicked);
connect(this, &QTreeWidget::itemDoubleClicked,
this, &Library::onItemDoubleClicked);
}
void Library::buildStaticNodes()
{
// My Library
m_myLibNode = new QTreeWidgetItem(this, QStringList{tr("My Library")});
m_myLibNode->setExpanded(true);
auto *tracksItem = new QTreeWidgetItem(m_myLibNode, QStringList{tr("Favorite Tracks")});
tracksItem->setData(0, TypeRole, NodeFavTracks);
auto *albumsItem = new QTreeWidgetItem(m_myLibNode, QStringList{tr("Favorite Albums")});
albumsItem->setData(0, TypeRole, NodeFavAlbums);
auto *artistsItem = new QTreeWidgetItem(m_myLibNode, QStringList{tr("Favorite Artists")});
artistsItem->setData(0, TypeRole, NodeFavArtists);
// Playlists
m_playlistsNode = new QTreeWidgetItem(this, QStringList{tr("Playlists")});
m_playlistsNode->setExpanded(true);
}
void Library::refresh()
{
// Remove old playlist children
while (m_playlistsNode->childCount() > 0)
delete m_playlistsNode->takeChild(0);
m_backend->getUserPlaylists();
}
void Library::onUserPlaylistsLoaded(const QJsonObject &result)
{
while (m_playlistsNode->childCount() > 0)
delete m_playlistsNode->takeChild(0);
const QJsonArray items = result["items"].toArray();
for (const auto &v : items) {
const QJsonObject pl = v.toObject();
const QString name = pl["name"].toString();
const qint64 id = static_cast<qint64>(pl["id"].toDouble());
auto *item = new QTreeWidgetItem(m_playlistsNode, QStringList{name});
item->setData(0, TypeRole, NodePlaylist);
item->setData(0, IdRole, id);
item->setData(0, NameRole, name);
}
}
void Library::onItemClicked(QTreeWidgetItem *item, int)
{
if (!item) return;
const int type = item->data(0, TypeRole).toInt();
switch (type) {
case NodeFavTracks: emit favTracksRequested(); break;
case NodeFavAlbums: emit favAlbumsRequested(); break;
case NodeFavArtists: emit favArtistsRequested(); break;
case NodePlaylist: {
const qint64 id = item->data(0, IdRole).toLongLong();
const QString name = item->data(0, NameRole).toString();
emit playlistRequested(id, name);
break;
}
default: break;
}
}
void Library::onItemDoubleClicked(QTreeWidgetItem *item, int column)
{
onItemClicked(item, column);
}
} // namespace List