Something went wrong. Try again.
Mixxx DJ software with MCP, Subsonic/Navidrome streaming library support — browse and load tracks from your self-hosted music server mixxx.org
subsonic navidrome streaming audio dj mac linux midi
Something went wrong. Try again.
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423#include "network/webtask.h"
#include <QMimeDatabase>#include <QTimerEvent>#include <mutex> // std::once_flag
#include "moc_webtask.cpp"#include "util/logger.h"#include "util/thread_affinity.h"
namespace mixxx {
namespace network {
namespace {
const Logger kLogger("mixxx::network::WebTask");
constexpr int kInvalidTimerId = -1;
std::once_flag registerMetaTypesOnceFlag;
void registerMetaTypesOnce() { WebResponse::registerMetaType(); WebResponseWithContent::registerMetaType();}
int readStatusCode( const QNetworkReply& networkReply) { const QVariant statusCodeAttr = networkReply.attribute(QNetworkRequest::HttpStatusCodeAttribute); if (!statusCodeAttr.isValid()) { // No status code available return kHttpStatusCodeInvalid; } VERIFY_OR_DEBUG_ASSERT(statusCodeAttr.canConvert<int>()) { kLogger.warning() << "Invalid status code attribute" << statusCodeAttr; return kHttpStatusCodeInvalid; } bool statusCodeValid = false; const int statusCode = statusCodeAttr.toInt(&statusCodeValid); VERIFY_OR_DEBUG_ASSERT(statusCodeValid && HttpStatusCode_isValid(statusCode)) { kLogger.warning() << "Failed to read status code attribute" << statusCodeAttr; return kHttpStatusCodeInvalid; } return statusCode;}
} // anonymous namespace
/*static*/ void WebResponse::registerMetaType() { qRegisterMetaType<WebResponse>("mixxx::network::WebResponse");}
QDebug operator<<(QDebug dbg, const WebResponse& arg) { return dbg << "WebResponse{" << arg.m_requestUrl << arg.m_replyUrl << arg.m_statusCode << '}';}
/*static*/ void WebResponseWithContent::registerMetaType() { qRegisterMetaType<WebResponseWithContent>("mixxx::network::WebResponseWithContent");}
QDebug operator<<(QDebug dbg, const WebResponseWithContent& arg) { return dbg << "WebResponseWithContent{" << arg.m_response << arg.m_contentType << arg.m_contentData << '}';}
//staticQMimeType WebTask::readContentType( const QNetworkReply& reply) { const QVariant contentTypeHeader = reply.header(QNetworkRequest::ContentTypeHeader); if (!contentTypeHeader.isValid() || contentTypeHeader.isNull()) { if (reply.isReadable() && reply.bytesAvailable() > 0) { kLogger.warning() << "Missing content type header"; } return QMimeType(); } const QString contentTypeString = contentTypeHeader.toString(); const QString contentTypeWithoutParams = contentTypeString.left(contentTypeString.indexOf(';')); const QMimeType contentType = QMimeDatabase().mimeTypeForName(contentTypeWithoutParams); if (!contentType.isValid()) { kLogger.warning() << "Unknown content type" << contentTypeWithoutParams; } return contentType;}
//staticstd::optional<QByteArray> WebTask::readContentData( QNetworkReply* reply) { if (!reply->isReadable()) { return std::nullopt; } return reply->readAll();}
WebTask::WebTask( QNetworkAccessManager* networkAccessManager, QObject* parent) : NetworkTask(networkAccessManager, parent), m_state(State::Initial), m_timeoutTimerId(kInvalidTimerId), m_timeoutMillis(kNoTimeout) { std::call_once(registerMetaTypesOnceFlag, registerMetaTypesOnce);}
void WebTask::onNetworkError( QNetworkReply::NetworkError errorCode, const QString& errorString, const WebResponseWithContent& responseWithContent) { DEBUG_ASSERT(m_state == State::Failed || m_state == State::Pending); DEBUG_ASSERT(m_timeoutTimerId == kInvalidTimerId);
DEBUG_ASSERT(errorCode != QNetworkReply::NoError); switch (errorCode) { case QNetworkReply::OperationCanceledError: // Client-side timeout case QNetworkReply::TimeoutError: // Network or server-side timeout m_state = State::TimedOut; break; default: m_state = State::Failed; } DEBUG_ASSERT(hasTerminated());
emitNetworkError( errorCode, errorString, responseWithContent);}
void WebTask::emitNetworkError( QNetworkReply::NetworkError errorCode, const QString& errorString, const WebResponseWithContent& responseWithContent) { VERIFY_OR_DEBUG_ASSERT( isSignalFuncConnected(&WebTask::networkError)) { kLogger.warning() << this << "Unhandled network error signal" << errorCode << errorString << responseWithContent; deleteLater(); return; } emit networkError( errorCode, errorString, responseWithContent);}
void WebTask::slotStart(int timeoutMillis, int delayMillis) { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); if (isBusy()) { kLogger.warning() << "Task is still busy and cannot be started again"; return; }
// Reset state DEBUG_ASSERT(!m_pendingNetworkReplyWeakPtr); DEBUG_ASSERT(m_timeoutTimerId == kInvalidTimerId); m_timeoutMillis = kNoTimeout;
if (delayMillis > 0) { m_state = State::Starting; kLogger.debug() << this << "Scheduling next request after" << delayMillis << "ms"; DEBUG_ASSERT(m_timeoutTimerId == kInvalidTimerId); // When the task is is starting/delayed, the timeoutTimer // is used for scheduling the request that should happen // after the delay. Afterwards, the timemoutTimer is used for // the actual request timeout. m_timeoutTimerId = startTimer(delayMillis); DEBUG_ASSERT(m_timeoutTimerId != kInvalidTimerId); // Store timeout for later m_timeoutMillis = timeoutMillis; return; }
auto* const pNetworkAccessManager = m_networkAccessManagerWeakPtr.data(); VERIFY_OR_DEBUG_ASSERT(pNetworkAccessManager) { m_state = State::Failed; onNetworkError( QNetworkReply::NetworkSessionFailedError, tr("No network access"), WebResponseWithContent{}); return; } DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(pNetworkAccessManager);
kLogger.debug() << this << "Starting..."; m_timer.start();
m_pendingNetworkReplyWeakPtr = doStartNetworkRequest( pNetworkAccessManager, timeoutMillis); if (!m_pendingNetworkReplyWeakPtr) { m_state = State::Failed; onNetworkError( QNetworkReply::NetworkSessionFailedError, tr("The Network request has not been started"), WebResponseWithContent{}); return; }
m_state = State::Pending;
DEBUG_ASSERT(m_timeoutTimerId == kInvalidTimerId); if (timeoutMillis != kNoTimeout) { DEBUG_ASSERT(timeoutMillis > 0); m_timeoutTimerId = startTimer(timeoutMillis); DEBUG_ASSERT(m_timeoutTimerId != kInvalidTimerId); m_timeoutMillis = timeoutMillis; }
// It is not necessary to connect the QNetworkReply::error signal. // Network errors are also received through the QNetworkReply::finished signal. connect(m_pendingNetworkReplyWeakPtr.data(), &QNetworkReply::finished, this, &WebTask::slotNetworkReplyFinished, Qt::UniqueConnection);}
void WebTask::slotAbort() { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); if (!isBusy()) { DEBUG_ASSERT(m_timeoutTimerId == kInvalidTimerId); if (m_state == State::Initial) { kLogger.debug() << this << "Cannot abort task in Initial state"; } else { DEBUG_ASSERT(hasTerminated()); kLogger.debug() << this << "Cannot abort terminated task"; } return; }
kLogger.debug() << this << "Aborting...";
if (m_timeoutTimerId != kInvalidTimerId) { killTimer(m_timeoutTimerId); m_timeoutTimerId = kInvalidTimerId; m_timeoutMillis = kNoTimeout; }
QUrl requestUrl; auto* const pPendingNetworkReply = m_pendingNetworkReplyWeakPtr.data(); if (pPendingNetworkReply) { DEBUG_ASSERT(m_state == State::Pending); if (pPendingNetworkReply->isRunning()) { kLogger.debug() << this << "Aborting pending network reply after" << m_timer.elapsed().toIntegerMillis() << "ms"; pPendingNetworkReply->abort(); // Aborting a pending reply will immediately emit a network // error signal that gets handled in this thread before // continuing with the next statements. DEBUG_ASSERT(hasTerminated()); DEBUG_ASSERT(!m_pendingNetworkReplyWeakPtr); return; } kLogger.debug() << this << "Aborted pending network reply after" << m_timer.elapsed().toIntegerMillis() << "ms"; // Save the request URL for emitting the signal (see below) requestUrl = pPendingNetworkReply->request().url(); // Ensure that the aborted reply is scheduled for deletion when leaving // this scope. const auto pendingNetworkReplyDeleter = ScopedDeleteLater(pPendingNetworkReply); m_pendingNetworkReplyWeakPtr.clear(); doNetworkReplyAborted(pPendingNetworkReply); }
m_state = State::Aborted; emitAborted(requestUrl);}
void WebTask::timerEvent(QTimerEvent* event) { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); const auto timerId = event->timerId(); DEBUG_ASSERT(timerId != kInvalidTimerId); VERIFY_OR_DEBUG_ASSERT(timerId == m_timeoutTimerId) { return; }
killTimer(m_timeoutTimerId); m_timeoutTimerId = kInvalidTimerId;
if (m_state == State::Starting) { DEBUG_ASSERT(!m_pendingNetworkReplyWeakPtr); m_state = State::Initial; slotStart(m_timeoutMillis); return; }
if (hasTerminated()) { DEBUG_ASSERT(!m_pendingNetworkReplyWeakPtr); return; } DEBUG_ASSERT(m_state == State::Pending);
kLogger.info() << this << "Aborting after timed out"; // Trigger the regular abort workflow after a client-side // timeout occurred slotAbort();}
void WebTask::slotNetworkReplyFinished() { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
auto* const pFinishedNetworkReply = qobject_cast<QNetworkReply*>(sender()); VERIFY_OR_DEBUG_ASSERT(pFinishedNetworkReply) { return; } // Ensure that the received reply gets deleted eventually const auto finishedNetworkReplyDeleter = ScopedDeleteLater(pFinishedNetworkReply);
if (kLogger.debugEnabled()) { if (pFinishedNetworkReply->url() == pFinishedNetworkReply->request().url()) { kLogger.debug() << this << "Received reply for request" << pFinishedNetworkReply->url(); } else { // Redirected kLogger.debug() << this << "Received reply for redirected request" << pFinishedNetworkReply->request().url() << "->" << pFinishedNetworkReply->url(); } }
// Check correlation between pending and finished reply auto* const pPendingNetworkReply = m_pendingNetworkReplyWeakPtr.data(); VERIFY_OR_DEBUG_ASSERT(pPendingNetworkReply == pFinishedNetworkReply) { // Another or no reply is pending kLogger.warning() << this << "Discarding unexpected network reply:" << "finished =" << pFinishedNetworkReply << "pending =" << pPendingNetworkReply; return; } m_pendingNetworkReplyWeakPtr.clear();
DEBUG_ASSERT(m_state == State::Pending); kLogger.debug() << this << "Received network reply after" << m_timer.elapsed().toIntegerMillis() << "ms";
if (m_timeoutTimerId != kInvalidTimerId) { killTimer(m_timeoutTimerId); m_timeoutTimerId = kInvalidTimerId; }
const HttpStatusCode statusCode = readStatusCode(*pFinishedNetworkReply); if (pFinishedNetworkReply->error() != QNetworkReply::NetworkError::NoError) { m_state = State::Failed; onNetworkError(pFinishedNetworkReply, statusCode); return; }
m_state = State::Finished; doNetworkReplyFinished(pFinishedNetworkReply, statusCode);}
void WebTask::onNetworkError( QNetworkReply* pFinishedNetworkReply, HttpStatusCode statusCode) { onNetworkError( pFinishedNetworkReply->error(), pFinishedNetworkReply->errorString(), WebResponseWithContent{ WebResponse{ pFinishedNetworkReply->url(), pFinishedNetworkReply->request().url(), statusCode}, readContentType(*pFinishedNetworkReply), readContentData(pFinishedNetworkReply).value_or(QByteArray{}), }); DEBUG_ASSERT(hasTerminated()); return;}
} // namespace network
} // namespace mixxx