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

View File

@@ -0,0 +1,221 @@
#include "qobuzbackend.hpp"
#include <QJsonDocument>
#include <QJsonArray>
#include <QMetaObject>
QobuzBackend::QobuzBackend(QObject *parent)
: QObject(parent)
{
m_backend = qobuz_backend_new(&QobuzBackend::eventTrampoline, this);
m_positionTimer = new QTimer(this);
m_positionTimer->setInterval(500);
connect(m_positionTimer, &QTimer::timeout, this, &QobuzBackend::onPositionTick);
m_positionTimer->start();
}
QobuzBackend::~QobuzBackend()
{
if (m_backend) {
qobuz_backend_free(m_backend);
m_backend = nullptr;
}
}
// ---- auth ----
void QobuzBackend::login(const QString &email, const QString &password)
{
qobuz_backend_login(m_backend, email.toUtf8().constData(), password.toUtf8().constData());
}
void QobuzBackend::setToken(const QString &token)
{
qobuz_backend_set_token(m_backend, token.toUtf8().constData());
}
// ---- catalog ----
void QobuzBackend::search(const QString &query, quint32 offset, quint32 limit)
{
qobuz_backend_search(m_backend, query.toUtf8().constData(), offset, limit);
}
void QobuzBackend::getAlbum(const QString &albumId)
{
qobuz_backend_get_album(m_backend, albumId.toUtf8().constData());
}
void QobuzBackend::getArtist(qint64 artistId)
{
qobuz_backend_get_artist(m_backend, artistId);
}
void QobuzBackend::getPlaylist(qint64 playlistId, quint32 offset, quint32 limit)
{
qobuz_backend_get_playlist(m_backend, playlistId, offset, limit);
}
// ---- favorites ----
void QobuzBackend::getFavTracks(quint32 offset, quint32 limit)
{
qobuz_backend_get_fav_tracks(m_backend, offset, limit);
}
void QobuzBackend::getFavAlbums(quint32 offset, quint32 limit)
{
qobuz_backend_get_fav_albums(m_backend, offset, limit);
}
void QobuzBackend::getFavArtists(quint32 offset, quint32 limit)
{
qobuz_backend_get_fav_artists(m_backend, offset, limit);
}
void QobuzBackend::getUserPlaylists(quint32 offset, quint32 limit)
{
qobuz_backend_get_user_playlists(m_backend, offset, limit);
}
// ---- fav modification ----
void QobuzBackend::addFavTrack(qint64 trackId)
{
qobuz_backend_add_fav_track(m_backend, trackId);
}
void QobuzBackend::removeFavTrack(qint64 trackId)
{
qobuz_backend_remove_fav_track(m_backend, trackId);
}
void QobuzBackend::addFavAlbum(const QString &albumId)
{
qobuz_backend_add_fav_album(m_backend, albumId.toUtf8().constData());
}
void QobuzBackend::removeFavAlbum(const QString &albumId)
{
qobuz_backend_remove_fav_album(m_backend, albumId.toUtf8().constData());
}
// ---- playback ----
void QobuzBackend::playTrack(qint64 trackId, int formatId)
{
qobuz_backend_play_track(m_backend, trackId, formatId);
}
void QobuzBackend::pause()
{
qobuz_backend_pause(m_backend);
}
void QobuzBackend::resume()
{
qobuz_backend_resume(m_backend);
}
void QobuzBackend::stop()
{
qobuz_backend_stop(m_backend);
}
void QobuzBackend::setVolume(int volume)
{
qobuz_backend_set_volume(m_backend, static_cast<quint8>(qBound(0, volume, 100)));
}
quint64 QobuzBackend::position() const { return qobuz_backend_get_position(m_backend); }
quint64 QobuzBackend::duration() const { return qobuz_backend_get_duration(m_backend); }
int QobuzBackend::volume() const { return qobuz_backend_get_volume(m_backend); }
int QobuzBackend::state() const { return qobuz_backend_get_state(m_backend); }
// ---- private slots ----
void QobuzBackend::onPositionTick()
{
emit positionChanged(position(), duration());
if (qobuz_backend_take_track_finished(m_backend))
emit trackFinished();
}
void QobuzBackend::onEvent(int eventType, const QString &json)
{
const QJsonObject obj = QJsonDocument::fromJson(json.toUtf8()).object();
switch (eventType) {
case EV_LOGIN_OK:
emit loginSuccess(obj["token"].toString(), obj["user"].toObject());
break;
case EV_LOGIN_ERR:
emit loginError(obj["error"].toString());
break;
case EV_SEARCH_OK:
emit searchResult(obj);
break;
case EV_SEARCH_ERR:
emit error(obj["error"].toString());
break;
case EV_ALBUM_OK:
emit albumLoaded(obj);
break;
case EV_ALBUM_ERR:
emit error(obj["error"].toString());
break;
case EV_ARTIST_OK:
emit artistLoaded(obj);
break;
case EV_ARTIST_ERR:
emit error(obj["error"].toString());
break;
case EV_PLAYLIST_OK:
emit playlistLoaded(obj);
break;
case EV_PLAYLIST_ERR:
emit error(obj["error"].toString());
break;
case EV_FAV_TRACKS_OK:
emit favTracksLoaded(obj);
break;
case EV_FAV_ALBUMS_OK:
emit favAlbumsLoaded(obj);
break;
case EV_FAV_ARTISTS_OK:
emit favArtistsLoaded(obj);
break;
case EV_PLAYLISTS_OK:
emit userPlaylistsLoaded(obj);
break;
case EV_TRACK_CHANGED:
emit trackChanged(obj);
break;
case EV_STATE_CHANGED:
emit stateChanged(obj["state"].toString());
break;
case EV_GENERIC_ERR:
case EV_TRACK_URL_ERR:
emit error(obj["error"].toString());
break;
default:
break;
}
}
// ---- static trampoline ----
void QobuzBackend::eventTrampoline(void *userdata, int eventType, const char *json)
{
auto *self = static_cast<QobuzBackend *>(userdata);
// Marshal from Rust thread → Qt main thread
QMetaObject::invokeMethod(
self,
"onEvent",
Qt::QueuedConnection,
Q_ARG(int, eventType),
Q_ARG(QString, QString::fromUtf8(json))
);
}