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:
221
src/backend/qobuzbackend.cpp
Normal file
221
src/backend/qobuzbackend.cpp
Normal 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))
|
||||
);
|
||||
}
|
||||
94
src/backend/qobuzbackend.hpp
Normal file
94
src/backend/qobuzbackend.hpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include "qobuz_backend.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
|
||||
/// Qt wrapper around the Rust qobuz-backend static library.
|
||||
///
|
||||
/// All signals are emitted on the Qt main thread regardless of which thread
|
||||
/// the Rust callback fires on (marshalled via QMetaObject::invokeMethod with
|
||||
/// Qt::QueuedConnection).
|
||||
class QobuzBackend : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QobuzBackend(QObject *parent = nullptr);
|
||||
~QobuzBackend() override;
|
||||
|
||||
// --- auth ---
|
||||
void login(const QString &email, const QString &password);
|
||||
void setToken(const QString &token);
|
||||
|
||||
// --- catalog ---
|
||||
void search(const QString &query, quint32 offset = 0, quint32 limit = 20);
|
||||
void getAlbum(const QString &albumId);
|
||||
void getArtist(qint64 artistId);
|
||||
void getPlaylist(qint64 playlistId, quint32 offset = 0, quint32 limit = 500);
|
||||
|
||||
// --- favorites ---
|
||||
void getFavTracks(quint32 offset = 0, quint32 limit = 500);
|
||||
void getFavAlbums(quint32 offset = 0, quint32 limit = 200);
|
||||
void getFavArtists(quint32 offset = 0, quint32 limit = 200);
|
||||
void getUserPlaylists(quint32 offset = 0, quint32 limit = 200);
|
||||
|
||||
// --- fav modification ---
|
||||
void addFavTrack(qint64 trackId);
|
||||
void removeFavTrack(qint64 trackId);
|
||||
void addFavAlbum(const QString &albumId);
|
||||
void removeFavAlbum(const QString &albumId);
|
||||
|
||||
// --- playback ---
|
||||
void playTrack(qint64 trackId, int formatId = 6);
|
||||
void pause();
|
||||
void resume();
|
||||
void stop();
|
||||
void setVolume(int volume);
|
||||
|
||||
quint64 position() const;
|
||||
quint64 duration() const;
|
||||
int volume() const;
|
||||
/// 1 = playing, 2 = paused, 0 = idle
|
||||
int state() const;
|
||||
|
||||
signals:
|
||||
// auth
|
||||
void loginSuccess(const QString &token, const QJsonObject &user);
|
||||
void loginError(const QString &error);
|
||||
|
||||
// catalog
|
||||
void searchResult(const QJsonObject &result);
|
||||
void albumLoaded(const QJsonObject &album);
|
||||
void artistLoaded(const QJsonObject &artist);
|
||||
void playlistLoaded(const QJsonObject &playlist);
|
||||
|
||||
// favorites
|
||||
void favTracksLoaded(const QJsonObject &result);
|
||||
void favAlbumsLoaded(const QJsonObject &result);
|
||||
void favArtistsLoaded(const QJsonObject &result);
|
||||
void userPlaylistsLoaded(const QJsonObject &result);
|
||||
|
||||
// playback
|
||||
void trackChanged(const QJsonObject &track);
|
||||
void stateChanged(const QString &state);
|
||||
void positionChanged(quint64 position, quint64 duration);
|
||||
void trackFinished();
|
||||
|
||||
// errors
|
||||
void error(const QString &message);
|
||||
|
||||
private slots:
|
||||
Q_INVOKABLE void onEvent(int eventType, const QString &json);
|
||||
void onPositionTick();
|
||||
|
||||
private:
|
||||
QobuzBackendOpaque *m_backend = nullptr;
|
||||
QTimer *m_positionTimer = nullptr;
|
||||
|
||||
// Static trampoline called from Rust threads
|
||||
static void eventTrampoline(void *userdata, int eventType, const char *json);
|
||||
};
|
||||
Reference in New Issue
Block a user