diff --git a/bluos/bluos.cpp b/bluos/bluos.cpp index bda017eb..b6251496 100644 --- a/bluos/bluos.cpp +++ b/bluos/bluos.cpp @@ -77,44 +77,9 @@ QUuid BluOS::getStatus() return; } emit connectionChanged(true); - QXmlStreamReader xml; - xml.addData(reply->readAll()); - if (xml.hasError()) { - qCDebug(dcBluOS()) << "XML Error:" << xml.errorString(); - } - - StatusResponse statusResponse; - if (xml.readNextStartElement()) { - if (xml.name() == "status") { - while(xml.readNextStartElement()){ - if(xml.name() == "artist"){ - } else if(xml.name() == "artist"){ - statusResponse.Artist = xml.readElementText(); - } else if(xml.name() == "album"){ - statusResponse.Album = xml.readElementText(); - } else if(xml.name() == "name"){ - statusResponse.Name = xml.readElementText(); - } else if(xml.name() == "service"){ - statusResponse.Service = xml.readElementText(); - } else if(xml.name() == "serviceIcon"){ - statusResponse.ServiceIcon = xml.readElementText(); - } else if(xml.name() == "shuffle"){ - statusResponse.Shuffle = xml.readElementText().toInt(); - } else if(xml.name() == "repeat"){ - statusResponse.Shuffle = xml.readElementText().toInt(); - } else if(xml.name() == "state"){ - statusResponse.PlaybackState = xml.readElementText().toInt(); - } else if(xml.name() == "volume"){ - statusResponse.Volume = xml.readElementText().toInt(); - } else if(xml.name() == "mute"){ - statusResponse.Mute = xml.readElementText().toInt(); - } else { - xml.skipCurrentElement(); - } - } - } - } - emit statusReceived(statusResponse); + QByteArray data = reply->readAll(); + qCDebug(dcBluOS()) << "Get Status:" << data; + parseState(data); }); return requestId; } @@ -303,6 +268,68 @@ QUuid BluOS::listPresets() url.setPort(m_port); url.setPath("/Presets"); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + if (reply->error() == QNetworkReply::HostNotFoundError) { + emit connectionChanged(false); + } + + qCWarning(dcBluOS()) << "Request error:" << status << reply->errorString(); + return; + } + emit connectionChanged(true); + + QXmlStreamReader xml; + xml.addData(reply->readAll()); + if (xml.hasError()) { + qCDebug(dcBluOS()) << "XML Error:" << xml.errorString(); + return; + } + QList presetList; + if (xml.readNextStartElement()) { + if (xml.name() == "presets") { + while(xml.readNextStartElement()){ + if(xml.name() == "preset"){ + Preset preset; + if (xml.attributes().hasAttribute("id")) { + preset.Id = xml.attributes().value("id").toInt(); + } + if (xml.attributes().hasAttribute("name")) { + preset.Name = xml.attributes().value("name").toString(); + } + if (xml.attributes().hasAttribute("url")) { + preset.Url = xml.attributes().value("url").toString(); + } + presetList.append(preset); + } else { + xml.skipCurrentElement(); + } + } + } + } + emit presetsReceived(requestId, presetList); + }); + + return requestId; +} + +QUuid BluOS::loadPreset(int preset) +{ + QUuid requestId = QUuid::createUuid(); + + QUrl url; + url.setScheme("http"); + url.setHost(m_hostAddress.toString()); + url.setPort(m_port); + url.setPath("/Presets"); + QUrlQuery query; + query.addQueryItem("id", QString::number(preset)); + url.setQuery(query); + QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, [reply, this] { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -318,23 +345,18 @@ QUuid BluOS::listPresets() } emit connectionChanged(true); }); - return requestId; } -QUuid BluOS::loadPreset(int preset) +QUuid BluOS::getSources() { - Q_UNUSED(preset) QUuid requestId = QUuid::createUuid(); QUrl url; url.setScheme("http"); url.setHost(m_hostAddress.toString()); url.setPort(m_port); - url.setPath("/Presets"); - QUrlQuery query; - query.addQueryItem("id", QString::number(preset)); - url.setQuery(query); + url.setPath("/Browse"); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, [reply, this] { reply->deleteLater(); @@ -470,12 +492,69 @@ QUuid BluOS::playBackControl(BluOS::PlaybackCommand command) } emit connectionChanged(true); - QXmlStreamReader xml; - xml.addData(reply->readAll()); - if (xml.hasError()) { - qCDebug(dcBluOS()) << "XML Error:" << xml.errorString(); - } - emit actionExecuted(requestId, true); + QByteArray data = reply->readAll(); + parseState(data); }); return requestId; } + +bool BluOS::parseState(const QByteArray &state) +{ + QXmlStreamReader xml; + xml.addData(state); + if (xml.hasError()) { + qCDebug(dcBluOS()) << "XML Error:" << xml.errorString(); + return false; + } + + StatusResponse statusResponse; + if (xml.readNextStartElement()) { + if (xml.name() == "status") { + while(xml.readNextStartElement()){ + if(xml.name() == "artist"){ + statusResponse.Artist = xml.readElementText(); + } else if(xml.name() == "album"){ + statusResponse.Album = xml.readElementText(); + } else if(xml.name() == "name"){ + statusResponse.Name = xml.readElementText(); + } else if(xml.name() == "service"){ + statusResponse.Service = xml.readElementText(); + } else if(xml.name() == "serviceIcon"){ + statusResponse.ServiceIcon = xml.readElementText(); + } else if(xml.name() == "shuffle"){ + statusResponse.Shuffle = xml.readElementText().toInt(); + } else if(xml.name() == "repeat"){ + statusResponse.Shuffle = xml.readElementText().toInt(); + } else if(xml.name() == "state"){ + QString playback = xml.readElementText(); + if (playback == "play") { + statusResponse.State = PlaybackState::Playing; + } else if (playback == "pause") { + statusResponse.State = PlaybackState::Paused; + } else if (playback == "stop") { + statusResponse.State = PlaybackState::Stopped; + } else if (playback == "connecting") { + statusResponse.State = PlaybackState::Connecting; + } else if (playback == "stream") { + statusResponse.State = PlaybackState::Streaming; + } else { + statusResponse.State = PlaybackState::Stopped; + qCWarning(dcBluOS()) << "State response, unhandled playback mode" << playback; + } + } else if(xml.name() == "volume"){ + statusResponse.Volume = xml.readElementText().toInt(); + } else if(xml.name() == "mute"){ + statusResponse.Mute = xml.readElementText().toInt(); + } else if(xml.name() == "image") { + statusResponse.Image = xml.readElementText(); + } else if(xml.name() == "title1") { + statusResponse.Title = xml.readElementText(); + } else { + xml.skipCurrentElement(); + } + } + } + } + emit statusReceived(statusResponse); + return true; +} diff --git a/bluos/bluos.h b/bluos/bluos.h index 59631aca..0dacabcd 100644 --- a/bluos/bluos.h +++ b/bluos/bluos.h @@ -58,25 +58,42 @@ public: None }; + enum PlaybackState { + Playing, + Paused, + Stopped, + Connecting, + Streaming + }; + struct StatusResponse { QString Album; QString Artist; QString Name; + QString Title; QString Service; QUrl ServiceIcon; - QString PlaybackState; + PlaybackState State; QUrl StationUrl; int Volume; bool Mute; RepeatMode Repeat; bool Shuffle; + QUrl Image; }; struct Preset { - int Prid; - QString name; + QString Name; int Id; - QString url; + QString Url; + }; + + struct Source { + // + QString Image; + QString BrowseKey; + QString Text; + QString Type; }; explicit BluOS(NetworkAccessManager *networkManager, QHostAddress hostAddress, int port, QObject *parent = nullptr); @@ -104,6 +121,7 @@ public: QUuid loadPreset(int preset); //1 for next preset, -1 for previous preset // Content Browsing + QUuid getSources(); // Player Grouping QUuid addGroupPlayer(QHostAddress address, int port); //adds player as slave @@ -115,13 +133,16 @@ private: NetworkAccessManager *m_networkManager = nullptr; QUuid playBackControl(PlaybackCommand command); + bool parseState(const QByteArray &state); signals: void connectionChanged(bool connected); void actionExecuted(QUuid actionId, bool success); - void presetsReceived(const QList &presets); void statusReceived(const StatusResponse &status); void volumeReceived(int volume, bool mute); + + void presetsReceived(QUuid requestId, const QList &presets); + void sourcesReceived(QUuid requestId, const QList &sources); }; #endif // BLUOS_H diff --git a/bluos/integrationpluginbluos.cpp b/bluos/integrationpluginbluos.cpp index f7a0efe3..6b229152 100644 --- a/bluos/integrationpluginbluos.cpp +++ b/bluos/integrationpluginbluos.cpp @@ -33,7 +33,8 @@ #include "plugininfo.h" #include "integrations/thing.h" #include "network/networkaccessmanager.h" - +#include "types/mediabrowseritem.h" +#include "types/browseritem.h" #include #include @@ -95,6 +96,9 @@ void IntegrationPluginBluOS::setupThing(ThingSetupInfo *info) connect(bluos, &BluOS::connectionChanged, this, &IntegrationPluginBluOS::onConnectionChanged); connect(bluos, &BluOS::statusReceived, this, &IntegrationPluginBluOS::onStatusResponseReceived); connect(bluos, &BluOS::actionExecuted, this, &IntegrationPluginBluOS::onActionExecuted); + connect(bluos, &BluOS::volumeReceived, this, &IntegrationPluginBluOS::onVolumeReceived); + connect(bluos, &BluOS::presetsReceived, this, &IntegrationPluginBluOS::onPresetsReceived); + connect(bluos, &BluOS::sourcesReceived, this, &IntegrationPluginBluOS::onSourcesReceived); m_asyncSetup.insert(bluos, info); bluos->getStatus(); @@ -114,8 +118,12 @@ void IntegrationPluginBluOS::postSetupThing(Thing *thing) Q_UNUSED(thing); if (!m_pluginTimer) { - //m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); - //connect(m_pluginTimer, &PluginTimer::timeout, this, &IntegrationPluginBluOS::onPluginTimer); + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); + connect(m_pluginTimer, &PluginTimer::timeout, [this] { + foreach(BluOS *bluos, m_bluos) { + bluos->getStatus(); + } + }); } } @@ -148,6 +156,8 @@ void IntegrationPluginBluOS::executeAction(ThingActionInfo *info) requestId = bluos->pause(); } else if (playbakStatus == "Stopped") { requestId = bluos->stop(); + } else { + qCWarning(dcBluOS()) << "Unhandled Playback mode"; } m_asyncActions.insert(requestId, info); connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);}); @@ -183,7 +193,7 @@ void IntegrationPluginBluOS::executeAction(ThingActionInfo *info) connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);}); } else if (action.actionTypeId() == bluosPlayerShuffleActionTypeId) { bool shuffle = action.param(bluosPlayerShuffleActionShuffleParamTypeId).value().toBool(); - QUuid requestId = bluos->setMute(shuffle); + QUuid requestId = bluos->setShuffle(shuffle); m_asyncActions.insert(requestId, info); connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);}); } else if (action.actionTypeId() == bluosPlayerRepeatActionTypeId) { @@ -195,6 +205,8 @@ void IntegrationPluginBluOS::executeAction(ThingActionInfo *info) requestId = bluos->setRepeat(BluOS::RepeatMode::All); } else if (repeat == "None") { requestId = bluos->setRepeat(BluOS::RepeatMode::None); + } else { + qCWarning(dcBluOS()) << "Unhandled Repeat Mode"; } m_asyncActions.insert(requestId, info); connect(info, &ThingActionInfo::aborted, [this, requestId] {m_asyncActions.remove(requestId);}); @@ -210,12 +222,54 @@ void IntegrationPluginBluOS::executeAction(ThingActionInfo *info) void IntegrationPluginBluOS::browseThing(BrowseResult *result) { - Q_UNUSED(result) + Thing *thing = result->thing(); + if (thing->thingClassId() == bluosPlayerThingClassId) { + BluOS *bluos = m_bluos.value(thing->id()); + if (!bluos) { + return; + } + if (result->itemId() == "presets") { + QUuid requestId = bluos->listPresets(); + m_asyncBrowseResults.insert(requestId, result); + connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);}); + } else { + MediaBrowserItem presetItem("presets", "Presets", true, false); + presetItem.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites); + presetItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconMusicLibrary); + result->addItem(presetItem); + + MediaBrowserItem groupingItem("grouping", "Grouping", true, false); + presetItem.setIcon(BrowserItem::BrowserIcon::BrowserIconApplication); + presetItem.setMediaIcon(MediaBrowserItem::MediaBrowserIconNetwork); + result->addItem(presetItem); + + QUuid requestId = bluos->getSources(); + m_asyncBrowseResults.insert(requestId, result); + connect(result, &BrowseResult::aborted, this, [this, requestId]{m_asyncBrowseResults.remove(requestId);}); + } + } } void IntegrationPluginBluOS::browserItem(BrowserItemResult *result) { - Q_UNUSED(result) + Thing *thing = result->thing(); + if (thing->thingClassId() == bluosPlayerThingClassId) { + BluOS *bluos = m_bluos.value(thing->id()); + if (!bluos) { + return; + } + if (result->itemId() == "presets") { + QUuid requestId = bluos->listPresets(); + m_asyncBrowseItemResults.insert(requestId, result); + connect(result, &BrowserItemResult::aborted, this, [this, requestId]{m_asyncBrowseItemResults.remove(requestId);}); + } else { + BrowserItem presetItem("presets", "Presets", true, false); + presetItem.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites); + QUuid requestId = bluos->getSources(); + m_asyncBrowseItemResults.insert(requestId, result); + connect(result, &BrowserItemResult::aborted, this, [this, requestId]{m_asyncBrowseItemResults.remove(requestId);}); + } + } } void IntegrationPluginBluOS::executeBrowserItem(BrowserActionInfo *info) @@ -231,9 +285,10 @@ void IntegrationPluginBluOS::onConnectionChanged(bool connected) ThingSetupInfo *info = m_asyncSetup.take(bluos); if (connected) { m_bluos.insert(info->thing()->id(), bluos); + info->thing()->setStateValue(bluosPlayerConnectedStateTypeId, true); info->finish(Thing::ThingErrorNoError); } else { - info->finish(Thing::ThingErrorHardwareNotAvailable); + info->finish(Thing::ThingErrorSetupFailed); } } else { Thing *thing = myThings().findById(m_bluos.key(bluos)); @@ -251,23 +306,38 @@ void IntegrationPluginBluOS::onStatusResponseReceived(const BluOS::StatusRespons return; thing->setStateValue(bluosPlayerArtistStateTypeId, status.Artist); thing->setStateValue(bluosPlayerCollectionStateTypeId, status.Album); - thing->setStateValue(bluosPlayerTitleStateTypeId, status.Name); + thing->setStateValue(bluosPlayerTitleStateTypeId, status.Title); thing->setStateValue(bluosPlayerSourceStateTypeId, status.Service); - thing->setStateValue(bluosPlayerArtworkStateTypeId, status.ServiceIcon); - thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, status.PlaybackState); + thing->setStateValue(bluosPlayerArtworkStateTypeId, status.Image); + switch (status.State) { + case BluOS::PlaybackState::Playing: + case BluOS::PlaybackState::Streaming: + thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Playing"); + break; + case BluOS::PlaybackState::Paused: + thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Paused"); + break; + case BluOS::PlaybackState::Stopped: + thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Stopped"); + break; + default: + thing->setStateValue(bluosPlayerPlaybackStatusStateTypeId, "Stopped"); + break; + } + thing->setStateValue(bluosPlayerMuteStateTypeId, status.Mute); thing->setStateValue(bluosPlayerVolumeStateTypeId, status.Volume); thing->setStateValue(bluosPlayerShuffleStateTypeId, status.Shuffle); switch (status.Repeat) { - case BluOS::RepeatMode::All: + case BluOS::RepeatMode::All: thing->setStateValue(bluosPlayerRepeatStateTypeId, "All"); break; case BluOS::RepeatMode::One: thing->setStateValue(bluosPlayerRepeatStateTypeId, "One"); - break; + break; case BluOS::RepeatMode::None: thing->setStateValue(bluosPlayerRepeatStateTypeId, "None"); - break; + break; } } @@ -292,3 +362,43 @@ void IntegrationPluginBluOS::onVolumeReceived(int volume, bool mute) thing->setStateValue(bluosPlayerMuteStateTypeId, mute); thing->setStateValue(bluosPlayerVolumeStateTypeId, volume); } + +void IntegrationPluginBluOS::onPresetsReceived(QUuid requestId, const QList &presets) +{ + BluOS *bluos = static_cast(sender()); + Thing *thing = myThings().findById(m_bluos.key(bluos)); + if (!thing) + return; + Q_UNUSED(presets) + if (m_asyncBrowseResults.contains(requestId)) { + BrowseResult *result = m_asyncBrowseResults.take(requestId); + foreach(BluOS::Preset preset, presets) { + BrowserItem item("presets&"+QString::number(preset.Id), preset.Name, false, true); + item.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites); + result->addItem(item); + } + result->finish(Thing::ThingErrorNoError); + } + if (m_asyncBrowseItemResults.contains(requestId)) { + BrowserItemResult *result = m_asyncBrowseItemResults.take(requestId); + Q_UNUSED(result) + } +} + +void IntegrationPluginBluOS::onSourcesReceived(QUuid requestId, const QList &sources) +{ + BluOS *bluos = static_cast(sender()); + Thing *thing = myThings().findById(m_bluos.key(bluos)); + if (!thing) + return; + if (m_asyncBrowseResults.contains(requestId)) { + BrowseResult *result = m_asyncBrowseResults.take(requestId); + foreach(BluOS::Source source, sources) { + BrowserItem item(source.BrowseKey, source.Text, false, true); + item.setIcon(BrowserItem::BrowserIcon::BrowserIconFavorites); + //TODO set media icons + result->addItem(item); + } + result->finish(Thing::ThingErrorNoError); + } +} diff --git a/bluos/integrationpluginbluos.h b/bluos/integrationpluginbluos.h index 6abd45d0..b027d7d1 100644 --- a/bluos/integrationpluginbluos.h +++ b/bluos/integrationpluginbluos.h @@ -36,6 +36,7 @@ #include "integrations/integrationplugin.h" #include "platform/platformzeroconfcontroller.h" #include "network/zeroconf/zeroconfservicebrowser.h" +#include "plugintimer.h" #include #include @@ -82,6 +83,9 @@ private slots: void onStatusResponseReceived(const BluOS::StatusResponse &status); void onActionExecuted(QUuid actionId, bool success); void onVolumeReceived(int volume, bool mute); + + void onPresetsReceived(QUuid requestId, const QList &presets); + void onSourcesReceived(QUuid requestId, const QList &sources); }; #endif // INTEGRATIONPLUGINBLUOS_H