1 /* 2 * Copyright (C) 2015-2019 Jean-Luc Barriere 3 * 4 * This file is part of Noson-App 5 * 6 * Noson is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * 11 * Noson is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with Noson. If not, see <http://www.gnu.org/licenses/>. 18 * 19 */ 20 21 #include "sonos.h" 22 #include "listmodel.h" 23 #include "alarmsmodel.h" 24 25 #include <noson/requestbroker.h> 26 #include <noson/imageservice.h> 27 #include <noson/filestreamer.h> 28 #ifdef HAVE_PULSEAUDIO 29 #include <noson/pulsestreamer.h> 30 #endif 31 32 #include <QString> 33 #include <QDebug> 34 35 #define THREAD_EXPIRY_TIMEOUT 10000 36 #define DEFAULT_MAX_THREAD 16 37 38 using namespace nosonapp; 39 40 Sonos::Sonos(QObject* parent) 41 : QObject(parent) 42 , m_library(ManagedContents()) 43 , m_shareUpdateID(0) 44 , m_shareIndexInProgess(false) 45 , m_savedQueuesUpdateID(0) 46 , m_system(this, systemEventCB) 47 , m_workerPool() 48 , m_jobCount(0) 49 , m_locale("en_US") 50 { 51 SONOS::System::Debug(2); 52 // Set the local URI of my http listener 53 m_systemLocalURI = QString::fromUtf8(m_system.GetSystemLocalUri().c_str()); 54 // Register handlers to process remote request 55 SONOS::RequestBrokerPtr imageService(new SONOS::ImageService()); 56 m_system.RegisterRequestBroker(imageService); 57 #ifdef HAVE_PULSEAUDIO 58 m_system.RegisterRequestBroker(SONOS::RequestBrokerPtr(new SONOS::PulseStreamer(imageService.get()))); 59 #endif 60 m_system.RegisterRequestBroker(SONOS::RequestBrokerPtr(new SONOS::FileStreamer())); 61 62 m_workerPool.setExpiryTimeout(THREAD_EXPIRY_TIMEOUT); 63 m_workerPool.setMaxThreadCount(DEFAULT_MAX_THREAD); 64 } 65 66 Sonos::~Sonos() 67 { 68 { 69 // deregister the remaining contents before destroying this 70 auto left = m_library.Get(); 71 for (ManagedContents::iterator it = left->begin(); it != left->end(); ++it) 72 unregisterContent(*left, it->model); 73 left->clear(); 74 } 75 m_workerPool.clear(); 76 } 77 78 void Sonos::debug(int debug) 79 { 80 m_system.Debug(debug); 81 } 82 83 Future* Sonos::tryInit(int debug) 84 { 85 return new Future(new PromiseInit(*this, debug), this); 86 } 87 88 Future* Sonos::tryRenewSubscriptions() 89 { 90 return new Future(new PromiseRenewSubscriptions(*this), this); 91 } 92 93 Future* Sonos::tryJoinZones(const QVariantList& zonePayloads, const QVariant& toZonePayload) 94 { 95 return new Future(new PromiseJoinZones(*this, zonePayloads, toZonePayload), this); 96 } 97 98 Future* Sonos::tryUnjoinZone(const QVariant& zonePayload) 99 { 100 return new Future(new PromiseUnjoinZone(*this, zonePayload), this); 101 } 102 103 Future* Sonos::tryUnjoinRooms(const QVariantList& roomPayloads) 104 { 105 return new Future(new PromiseUnjoinRooms(*this, roomPayloads), this); 106 } 107 108 Future* Sonos::tryCreateAlarm(const QVariant& alarmPayload) 109 { 110 return new Future(new PromiseCreateAlarm(*this, alarmPayload), this); 111 } 112 113 Future* Sonos::tryUpdateAlarm(const QVariant& alarmPayload) 114 { 115 return new Future(new PromiseUpdateAlarm(*this, alarmPayload), this); 116 } 117 118 Future* Sonos::tryDestroyAlarm(const QString& id) 119 { 120 return new Future(new PromiseDestroyAlarm(*this, id), this); 121 } 122 123 Future* Sonos::tryRefreshShareIndex() 124 { 125 return new Future(new PromiseRefreshShareIndex(*this), this); 126 } 127 128 Future* Sonos::tryDestroySavedQueue(const QString& SQid) 129 { 130 return new Future(new PromiseDestroySavedQueue(*this, SQid), this); 131 } 132 133 Future* Sonos::tryAddItemToFavorites(const QVariant& payload, const QString& description, const QString& artURI) 134 { 135 return new Future(new PromiseAddItemToFavorites(*this, payload, description, artURI), this); 136 } 137 138 Future* Sonos::tryDestroyFavorite(const QString& FVid) 139 { 140 return new Future(new PromiseDestroyFavorite(*this, FVid), this); 141 } 142 143 bool Sonos::init(int debug /*= 0*/) 144 { 145 SONOS::System::Debug(2); 146 bool ret = m_system.Discover(); 147 SONOS::System::Debug(debug); 148 emit initDone(ret); 149 return ret; 150 } 151 152 bool Sonos::init(int debug, const QString& url) 153 { 154 SONOS::System::Debug(2); 155 bool ret = m_system.Discover(url.toStdString().c_str()); 156 SONOS::System::Debug(debug); 157 emit initDone(ret); 158 return ret; 159 } 160 161 void Sonos::setLocale(const QString& locale) 162 { 163 m_locale.Store(locale); 164 } 165 166 QString Sonos::getLocale() 167 { 168 return m_locale.Load(); 169 } 170 171 QString Sonos::getLibVersion() 172 { 173 return QString(LIBVERSION); 174 } 175 176 void Sonos::addServiceOAuth(const QString& type, const QString& sn, const QString& key, const QString& token, const QString& username) 177 { 178 SONOS::System::AddServiceOAuth(type.toUtf8().constData(), sn.toUtf8().constData(), key.toUtf8().constData(), token.toUtf8().constData(), username.toUtf8().constData()); 179 } 180 181 void Sonos::deleteServiceOAuth(const QString& type, const QString& sn) 182 { 183 SONOS::System::DeleteServiceOAuth(type.toUtf8().constData(), sn.toUtf8().constData()); 184 } 185 186 void Sonos::renewSubscriptions() 187 { 188 m_system.RenewSubscriptions(); 189 } 190 191 QVariantList Sonos::getZones() 192 { 193 ZonesModel model; 194 model.init(this, true); 195 model.resetModel(); 196 QVariantList list; 197 for (int r = 0; r < model.rowCount(); ++r) 198 list.append(model.get(r)); 199 return list; 200 } 201 202 bool Sonos::isConnected() 203 { 204 return m_system.IsConnected(); 205 } 206 207 QVariantList Sonos::getZoneRooms(const QString& zoneId) 208 { 209 RoomsModel model; 210 model.load(this, zoneId); 211 QVariantList list; 212 for (int r = 0; r < model.rowCount(); ++r) 213 list.append(model.get(r)); 214 return list; 215 } 216 217 bool Sonos::joinRoom(const QVariant& roomPayload, const QVariant& toZonePayload) 218 { 219 SONOS::ZonePlayerPtr room = roomPayload.value<SONOS::ZonePlayerPtr>(); 220 SONOS::ZonePtr zone = toZonePayload.value<SONOS::ZonePtr>(); 221 if (room && room->IsValid() && zone && zone->GetCoordinator()) 222 { 223 SONOS::Player player(room); 224 return player.JoinToGroup(zone->GetCoordinator()->GetUUID()); 225 } 226 return false; 227 } 228 229 bool Sonos::joinZone(const QVariant& zonePayload, const QVariant& toZonePayload) 230 { 231 SONOS::ZonePtr zone = zonePayload.value<SONOS::ZonePtr>(); 232 SONOS::ZonePtr toZone = toZonePayload.value<SONOS::ZonePtr>(); 233 if (zone && toZone && toZone->GetCoordinator()) 234 { 235 for (std::vector<SONOS::ZonePlayerPtr>::iterator it = zone->begin(); it != zone->end(); ++it) 236 { 237 SONOS::Player player(*it); 238 player.JoinToGroup(toZone->GetCoordinator()->GetUUID()); 239 } 240 return true; 241 } 242 return false; 243 244 } 245 246 bool Sonos::joinZones(const QVariantList& zonePayloads, const QVariant& toZonePayload) 247 { 248 std::vector<SONOS::ZonePtr> zones; 249 SONOS::ZonePtr toZone = toZonePayload.value<SONOS::ZonePtr>(); 250 for (QVariantList::const_iterator it = zonePayloads.begin(); it != zonePayloads.end(); ++it) 251 zones.push_back(it->value<SONOS::ZonePtr>()); 252 if (toZone && toZone->GetCoordinator()) 253 { 254 for (std::vector<SONOS::ZonePtr>::const_iterator it = zones.begin(); it != zones.end(); ++it) 255 { 256 if ((*it)->GetZoneName() == toZone->GetZoneName()) 257 continue; 258 for (std::vector<SONOS::ZonePlayerPtr>::iterator itr = (*it)->begin(); itr != (*it)->end(); ++itr) 259 { 260 SONOS::Player player(*itr); 261 player.JoinToGroup(toZone->GetCoordinator()->GetUUID()); 262 } 263 } 264 return true; 265 } 266 return false; 267 } 268 269 bool Sonos::unjoinRoom(const QVariant& roomPayload) 270 { 271 SONOS::ZonePlayerPtr room = roomPayload.value<SONOS::ZonePlayerPtr>(); 272 if (room && room->IsValid()) 273 { 274 SONOS::Player player(room); 275 return player.BecomeStandalone(); 276 } 277 return false; 278 } 279 280 bool Sonos::unjoinRooms(const QVariantList& roomPayloads) 281 { 282 for (QVariantList::const_iterator it = roomPayloads.begin(); it != roomPayloads.end(); ++it) { 283 SONOS::ZonePlayerPtr room = it->value<SONOS::ZonePlayerPtr>(); 284 if (room && room->IsValid()) 285 { 286 SONOS::Player player(room); 287 return player.BecomeStandalone(); 288 } 289 else 290 return false; 291 } 292 return true; 293 } 294 295 bool Sonos::unjoinZone(const QVariant& zonePayload) 296 { 297 SONOS::ZonePtr zone = zonePayload.value<SONOS::ZonePtr>(); 298 if (zone) 299 { 300 for (std::vector<SONOS::ZonePlayerPtr>::iterator it = zone->begin(); it != zone->end(); ++it) 301 { 302 SONOS::Player player(*it); 303 player.BecomeStandalone(); 304 } 305 return true; 306 } 307 return false; 308 309 } 310 311 bool Sonos::createAlarm(const QVariant& alarmPayload) 312 { 313 SONOS::AlarmPtr ptr = alarmPayload.value<SONOS::AlarmPtr>(); 314 if (ptr && m_system.CreateAlarm(*ptr)) 315 { 316 return true; 317 } 318 return false; 319 } 320 321 bool Sonos::updateAlarm(const QVariant& alarmPayload) 322 { 323 SONOS::AlarmPtr ptr = alarmPayload.value<SONOS::AlarmPtr>(); 324 if (ptr && m_system.UpdateAlarm(*ptr)) 325 { 326 return true; 327 } 328 return false; 329 } 330 331 bool Sonos::destroyAlarm(const QString& id) 332 { 333 return m_system.DestroyAlarm(id.toUtf8().constData()); 334 } 335 336 bool Sonos::refreshShareIndex() 337 { 338 return m_system.RefreshShareIndex(); 339 } 340 341 bool Sonos::destroySavedQueue(const QString& SQid) 342 { 343 return m_system.DestroySavedQueue(SQid.toUtf8().constData()); 344 } 345 346 bool Sonos::addItemToFavorites(const QVariant& payload, const QString& description, const QString& artURI) 347 { 348 return m_system.AddURIToFavorites(payload.value<SONOS::DigitalItemPtr>(), description.toUtf8().constData(), artURI.toUtf8().constData()); 349 } 350 351 bool Sonos::destroyFavorite(const QString& FVid) 352 { 353 return m_system.DestroyFavorite(FVid.toUtf8().constData()); 354 } 355 356 QString Sonos::getObjectIDFromUriMetadata(const QVariant& itemPayload) 357 { 358 SONOS::DigitalItemPtr ptr = itemPayload.value<SONOS::DigitalItemPtr>(); 359 return QString::fromUtf8(m_system.GetObjectIDFromUriMetadata(ptr).c_str()); 360 } 361 362 bool Sonos::isItemFromService(const QVariant &itemPayload) 363 { 364 SONOS::DigitalItemPtr ptr = itemPayload.value<SONOS::DigitalItemPtr>(); 365 if (ptr && m_system.IsItemFromService(ptr)) 366 { 367 return true; 368 } 369 return false; 370 } 371 372 SONOS::System &Sonos::getSystem() 373 { 374 return m_system; 375 } 376 377 SONOS::ZonePtr Sonos::findZone(const QString& zoneName) 378 { 379 std::string name = zoneName.toUtf8().constData(); 380 SONOS::ZoneList zones = m_system.GetZoneList(); 381 if (zones.empty()) 382 return SONOS::ZonePtr(); 383 // loop in zones 384 for (SONOS::ZoneList::const_iterator it = zones.begin(); it != zones.end(); ++it) 385 { 386 if (name.empty() || name == it->second->GetZoneName()) 387 return it->second; 388 // loop in group to search the player with the given name 389 for (std::vector<SONOS::ZonePlayerPtr>::const_iterator itp = it->second->begin(); itp != it->second->end(); ++itp) 390 { 391 if (name == **itp) 392 return it->second; 393 } 394 } 395 return zones.begin()->second; 396 } 397 398 void Sonos::runLoader() 399 { 400 m_workerPool.start(new ContentLoader<Sonos>(*this)); 401 } 402 403 void Sonos::beforeLoad() 404 { 405 beginJob(); 406 } 407 408 void Sonos::afterLoad() 409 { 410 endJob(); 411 } 412 413 void Sonos::runContentLoader(ListModel<Sonos>* model) 414 { 415 if (model && !model->m_pending) 416 { 417 model->m_pending = true; // decline next request 418 m_workerPool.start(new ContentLoader<Sonos>(*this, model)); 419 } 420 else 421 qWarning("%s: request has been declined (%p)", __FUNCTION__, model); 422 } 423 424 void Sonos::loadContent(ListModel<Sonos>* model) 425 { 426 auto mc = m_library.Get(); 427 for (ManagedContents::iterator it = mc->begin(); it != mc->end(); ++it) 428 if (it->model == model) 429 { 430 qDebug("%s: %p (%s)", __FUNCTION__, model, model->m_root.toUtf8().constData()); 431 emit loadingStarted(); 432 model->m_pending = false; // accept add next request in queue 433 model->loadData(); 434 emit loadingFinished(); 435 break; 436 } 437 } 438 439 void Sonos::loadAllContent() 440 { 441 QList<ListModel<Sonos>*> left; 442 { 443 auto mc = m_library.Get(); 444 for (ManagedContents::iterator it = mc->begin(); it != mc->end(); ++it) 445 if (it->model->m_dataState == DataStatus::DataNotFound) 446 left.push_back(it->model); 447 } 448 emit loadingStarted(); 449 if (!left.empty()) 450 { 451 while (!left.isEmpty()) 452 { 453 ListModel<Sonos>* model = left.front(); 454 model->loadData(); 455 left.pop_front(); 456 } 457 } 458 emit loadingFinished(); 459 } 460 461 void Sonos::runContentLoaderForContext(ListModel<Sonos>* model, int id) 462 { 463 if (model && !model->m_pending) 464 { 465 model->m_pending = true; // decline next request 466 m_workerPool.start(new ContentForContextLoader<Sonos>(*this, model, id)); 467 } 468 else 469 qWarning("%s: request id %d has been declined (%p)", __FUNCTION__, id, model); 470 } 471 472 void Sonos::loadContentForContext(ListModel<Sonos>* model, int id) 473 { 474 model->m_pending = false; // accept add next request in queue 475 model->loadDataForContext(id); 476 } 477 478 const char* Sonos::getHost() const 479 { 480 return m_system.GetHost().c_str(); 481 } 482 483 unsigned Sonos::getPort() const 484 { 485 return m_system.GetPort(); 486 } 487 488 QString Sonos::getBaseUrl() const 489 { 490 QString port; 491 port.setNum(m_system.GetPort()); 492 QString url = "http://"; 493 url.append(m_system.GetHost().c_str()).append(":").append(port); 494 return url; 495 } 496 497 void Sonos::registerContent(ListModel<Sonos>* model, const QString& root) 498 { 499 if (model) 500 { 501 qDebug("%s: %p (%s)", __FUNCTION__, model, root.toUtf8().constData()); 502 auto mc = m_library.Get(); 503 for (ManagedContents::iterator it = mc->begin(); it != mc->end(); ++it) 504 { 505 if (it->model == model) 506 { 507 it->root = root; 508 return; 509 } 510 } 511 mc->append(RegisteredContent<Sonos>(model, root)); 512 } 513 } 514 515 void Sonos::unregisterContent(ListModel<Sonos>* model) 516 { 517 auto mc = m_library.Get(); 518 unregisterContent(*mc, model); 519 } 520 521 bool Sonos::startJob(QRunnable* worker) 522 { 523 return m_workerPool.tryStart(worker); 524 } 525 526 void Sonos::beginJob() 527 { 528 m_jobCount.Add(1); 529 emit jobCountChanged(); 530 } 531 532 void Sonos::endJob() 533 { 534 m_jobCount.Add(-1); 535 emit jobCountChanged(); 536 } 537 538 void Sonos::systemEventCB(void *handle) 539 { 540 Sonos* sonos = static_cast<Sonos*>(handle); 541 Q_ASSERT(sonos); 542 // Read last event flags 543 unsigned char events = sonos->getSystem().LastEvents(); 544 545 if ((events & SONOS::SVCEvent_ZGTopologyChanged)) 546 emit sonos->topologyChanged(); 547 if ((events & SONOS::SVCEvent_AlarmClockChanged)) 548 emit sonos->alarmClockChanged(); 549 if ((events & SONOS::SVCEvent_ContentDirectoryChanged)) 550 { 551 auto cl = sonos->m_library.Get(); 552 SONOS::ContentProperty prop = sonos->getSystem().GetContentProperty(); 553 for (std::vector<std::pair<std::string, unsigned> >::const_iterator uit = prop.ContainerUpdateIDs.begin(); uit != prop.ContainerUpdateIDs.end(); ++uit) 554 { 555 qDebug("%s: container [%s] has being updated to %u", __FUNCTION__, uit->first.c_str(), uit->second); 556 557 bool shareUpdated = false; 558 bool savedQueuesUpdated = false; 559 560 // Reload musical index on any update of shares 561 if (uit->first == "S:" && uit->second != sonos->m_shareUpdateID) 562 { 563 shareUpdated = true; 564 sonos->m_shareUpdateID = uit->second; // Track current updateID 565 } 566 // Reload saved queues on any update 567 else if (uit->first == "SQ:" && uit->second != sonos->m_savedQueuesUpdateID) 568 { 569 savedQueuesUpdated = true; 570 sonos->m_savedQueuesUpdateID = uit->second; // Track current updateID 571 } 572 573 for (ManagedContents::iterator it = cl->begin(); it != cl->end(); ++it) 574 { 575 // find the base of the model from its root 576 QString _base; 577 int slash = it->model->m_root.indexOf("/"); 578 if (slash < 0) 579 _base.append(it->model->m_root); 580 else 581 _base.append(it->model->m_root.left(slash)); 582 583 // need update ? 584 bool _update = false; 585 // same base 586 if (it->model->m_updateID != uit->second && _base == uit->first.c_str()) 587 _update = true; 588 // about shares 589 else if (shareUpdated && _base.startsWith(QString::fromUtf8("A:"))) 590 _update = true; 591 else if (savedQueuesUpdated && _base.startsWith(QString::fromUtf8("SQ:"))) 592 _update = true; 593 594 if (_update) 595 it->model->handleDataUpdate(); 596 } 597 } 598 // Signal share index events 599 if (prop.ShareIndexInProgress != sonos->m_shareIndexInProgess) 600 { 601 if (prop.ShareIndexInProgress) 602 emit sonos->shareIndexInProgress(); 603 else 604 emit sonos->shareIndexFinished(); 605 sonos->m_shareIndexInProgess = prop.ShareIndexInProgress; 606 } 607 } 608 } 609 610 void Sonos::unregisterContent(ManagedContents& mc, ListModel<Sonos> *model) 611 { 612 if (model) 613 { 614 QList<ManagedContents::iterator> left; 615 for (ManagedContents::iterator it = mc.begin(); it != mc.end(); ++it) 616 if (it->model == model) 617 { 618 LockGuard<QRecursiveMutex> g(it->model->m_lock); 619 qDebug("%s: %p (%s)", __FUNCTION__, it->model, it->model->m_root.toUtf8().constData()); 620 it->model->m_provider = nullptr; 621 left.push_back(it); 622 } 623 for (QList<ManagedContents::iterator>::iterator itl = left.begin(); itl != left.end(); ++itl) 624 mc.erase(*itl); 625 } 626 } 627 628 /////////////////////////////////////////////////////////////////////////////// 629 /// 630 /// About promises 631 632 void Sonos::PromiseInit::run() 633 { 634 bool r = m_sonos.init(m_debug); 635 setResult(QVariant(r)); 636 } 637 638 void Sonos::PromiseRenewSubscriptions::run() 639 { 640 m_sonos.renewSubscriptions(); 641 setResult(QVariant(true)); 642 } 643 644 void Sonos::PromiseJoinZones::run() 645 { 646 bool r = m_sonos.joinZones(m_zonePayloads, m_toZonePayload); 647 setResult(QVariant(r)); 648 } 649 650 void Sonos::PromiseUnjoinZone::run() 651 { 652 bool r = m_sonos.unjoinZone(m_zonePayload); 653 setResult(QVariant(r)); 654 } 655 656 void Sonos::PromiseUnjoinRooms::run() 657 { 658 bool r = m_sonos.unjoinRooms(m_roomPayloads); 659 setResult(QVariant(r)); 660 } 661 662 void Sonos::PromiseCreateAlarm::run() 663 { 664 bool r = m_sonos.createAlarm(m_alarmPayload); 665 setResult(QVariant(r)); 666 } 667 668 void Sonos::PromiseUpdateAlarm::run() 669 { 670 bool r = m_sonos.updateAlarm(m_alarmPayload); 671 setResult(QVariant(r)); 672 } 673 674 void Sonos::PromiseDestroyAlarm::run() 675 { 676 bool r = m_sonos.destroyAlarm(m_id); 677 setResult(QVariant(r)); 678 } 679 680 void Sonos::PromiseRefreshShareIndex::run() 681 { 682 bool r = m_sonos.refreshShareIndex(); 683 setResult(QVariant(r)); 684 } 685 686 void Sonos::PromiseDestroySavedQueue::run() 687 { 688 bool r = m_sonos.destroySavedQueue(m_SQid); 689 setResult(QVariant(r)); 690 } 691 692 void Sonos::PromiseAddItemToFavorites::run() 693 { 694 bool r = m_sonos.addItemToFavorites(m_payload, m_description, m_artURI); 695 setResult(QVariant(r)); 696 } 697 698 void Sonos::PromiseDestroyFavorite::run() 699 { 700 bool r = m_sonos.destroyFavorite(m_FVid); 701 setResult(QVariant(r)); 702 } 703