MythTV  0.26-pre
upnpscanner.cpp
Go to the documentation of this file.
00001 #include <QCoreApplication>
00002 #include <QTextCodec>
00003 
00004 #include "mythcorecontext.h"
00005 #include "mythlogging.h"
00006 #include "ssdp.h"
00007 #include "upnpscanner.h"
00008 
00009 #define LOC QString("UPnPScan: ")
00010 #define ERR QString("UPnPScan error: ")
00011 
00012 #define MAX_ATTEMPTS 5
00013 #define MAX_REQUESTS 1
00014 
00015 QString MediaServerItem::NextUnbrowsed(void)
00016 {
00017     // items don't need scanning
00018     if (!m_url.isEmpty())
00019         return QString();
00020 
00021     // scan this container
00022     if (!m_scanned)
00023         return m_id;
00024 
00025     // scan children
00026     QMutableMapIterator<QString,MediaServerItem> it(m_children);
00027     while (it.hasNext())
00028     {
00029         it.next();
00030         QString result = it.value().NextUnbrowsed();
00031         if (!result.isEmpty())
00032             return result;
00033     }
00034 
00035     return QString();
00036 }
00037 
00038 MediaServerItem* MediaServerItem::Find(QString &id)
00039 {
00040     if (m_id == id)
00041         return this;
00042 
00043     QMutableMapIterator<QString,MediaServerItem> it(m_children);
00044     while (it.hasNext())
00045     {
00046         it.next();
00047         MediaServerItem* result = it.value().Find(id);
00048         if (result)
00049             return result;
00050     }
00051 
00052     return NULL;
00053 }
00054 
00055 bool MediaServerItem::Add(MediaServerItem &item)
00056 {
00057     if (m_id == item.m_parentid)
00058     {
00059         m_children.insert(item.m_id, item);
00060         return true;
00061     }
00062     return false;
00063 }
00064 
00065 void MediaServerItem::Reset(void)
00066 {
00067     m_children.clear();
00068     m_scanned = false;
00069 }
00070 
00075 class MediaServer : public MediaServerItem
00076 {
00077   public:
00078     MediaServer()
00079      : MediaServerItem(QString("0"), QString(), QString(), QString()),
00080        m_URL(), m_connectionAttempts(0), m_controlURL(QUrl()),
00081        m_eventSubURL(QUrl()), m_eventSubPath(QString()),
00082        m_friendlyName(QString("Unknown")), m_subscribed(false),
00083        m_renewalTimerId(0), m_systemUpdateID(-1)
00084     {
00085     }
00086     MediaServer(QUrl URL)
00087      : MediaServerItem(QString("0"), QString(), QString(), QString()),
00088        m_URL(URL), m_connectionAttempts(0), m_controlURL(QUrl()),
00089        m_eventSubURL(QUrl()), m_eventSubPath(QString()),
00090        m_friendlyName(QString("Unknown")), m_subscribed(false),
00091        m_renewalTimerId(0), m_systemUpdateID(-1)
00092     {
00093     }
00094 
00095     bool ResetContent(int new_id)
00096     {
00097         bool result = true;
00098         if (m_systemUpdateID != -1)
00099         {
00100             result = false;
00101             Reset();
00102         }
00103         m_systemUpdateID = new_id;
00104         return result;
00105     }
00106 
00107     QUrl    m_URL;
00108     int     m_connectionAttempts;
00109     QUrl    m_controlURL;
00110     QUrl    m_eventSubURL;
00111     QString m_eventSubPath;
00112     QString m_friendlyName;
00113     bool    m_subscribed;
00114     int     m_renewalTimerId;
00115     int     m_systemUpdateID;
00116 };
00117 
00118 UPNPScanner* UPNPScanner::gUPNPScanner        = NULL;
00119 bool         UPNPScanner::gUPNPScannerEnabled = false;
00120 MThread*     UPNPScanner::gUPNPScannerThread  = NULL;
00121 QMutex*      UPNPScanner::gUPNPScannerLock    = new QMutex(QMutex::Recursive);
00122 
00133 UPNPScanner::UPNPScanner(UPNPSubscription *sub)
00134   : QObject(), m_subscription(sub), m_lock(QMutex::Recursive),
00135     m_network(NULL), m_updateTimer(NULL), m_watchdogTimer(NULL),
00136     m_masterHost(QString()), m_masterPort(0), m_scanComplete(false),
00137     m_fullscan(false)
00138 {
00139 }
00140 
00141 UPNPScanner::~UPNPScanner()
00142 {
00143     Stop();
00144 }
00145 
00150 void UPNPScanner::Enable(bool enable, UPNPSubscription *sub)
00151 {
00152     QMutexLocker locker(gUPNPScannerLock);
00153     gUPNPScannerEnabled = enable;
00154     Instance(sub);
00155 }
00156 
00162 UPNPScanner* UPNPScanner::Instance(UPNPSubscription *sub)
00163 {
00164     QMutexLocker locker(gUPNPScannerLock);
00165     if (!gUPNPScannerEnabled)
00166     {
00167         if (gUPNPScannerThread)
00168         {
00169             gUPNPScannerThread->quit();
00170             gUPNPScannerThread->wait();
00171         }
00172         delete gUPNPScannerThread;
00173         gUPNPScannerThread = NULL;
00174         delete gUPNPScanner;
00175         gUPNPScanner = NULL;
00176         return NULL;
00177     }
00178 
00179     if (!gUPNPScannerThread)
00180         gUPNPScannerThread = new MThread("UPnPScanner");
00181     if (!gUPNPScanner)
00182         gUPNPScanner = new UPNPScanner(sub);
00183 
00184     if (!gUPNPScannerThread->isRunning())
00185     {
00186         gUPNPScanner->moveToThread(gUPNPScannerThread->qthread());
00187         QObject::connect(
00188             gUPNPScannerThread->qthread(), SIGNAL(started()),
00189             gUPNPScanner,                  SLOT(Start()));
00190         gUPNPScannerThread->start(QThread::LowestPriority);
00191     }
00192 
00193     return gUPNPScanner;
00194 }
00200 void UPNPScanner::StartFullScan(void)
00201 {
00202     m_fullscan = true;
00203     MythEvent *me = new MythEvent(QString("UPNP_STARTSCAN"));
00204     qApp->postEvent(this, me);
00205 }
00206 
00213 void UPNPScanner::GetInitialMetadata(VideoMetadataListManager::metadata_list* list,
00214                                      meta_dir_node *node)
00215 {
00216     // nothing to see..
00217     QMap<QString,QString> servers = ServerList();
00218     if (servers.isEmpty())
00219         return;
00220 
00221     // Add MediaServers
00222     LOG(VB_GENERAL, LOG_INFO, QString("Adding MediaServer metadata."));
00223 
00224     smart_dir_node mediaservers = node->addSubDir(tr("Media Servers"));
00225     mediaservers->setPathRoot();
00226 
00227     m_lock.lock();
00228     QMutableHashIterator<QString,MediaServer*> it(m_servers);
00229     while (it.hasNext())
00230     {
00231         it.next();
00232         if (!it.value()->m_subscribed)
00233             continue;
00234 
00235         QString usn = it.key();
00236         GetServerContent(usn, it.value(), list, mediaservers.get());
00237     }
00238     m_lock.unlock();
00239 }
00240 
00246 void UPNPScanner::GetMetadata(VideoMetadataListManager::metadata_list* list,
00247                               meta_dir_node *node)
00248 {
00249     // nothing to see..
00250     QMap<QString,QString> servers = ServerList();
00251     if (servers.isEmpty())
00252         return;
00253 
00254     // Start scanning if it isn't already running
00255     StartFullScan();
00256 
00257     // wait for the scanner to complete - with a 30 second timeout
00258     LOG(VB_GENERAL, LOG_INFO, LOC + "Waiting for scan to complete.");
00259 
00260     int count = 0;
00261     while (!m_scanComplete && (count++ < 300))
00262         usleep(100000);
00263 
00264     // some scans may just take too long (PlayOn)
00265     if (!m_scanComplete)
00266         LOG(VB_GENERAL, LOG_ERR, LOC + "MediaServer scan is incomplete.");
00267     else
00268         LOG(VB_GENERAL, LOG_INFO, LOC + "MediaServer scanning finished.");
00269 
00270 
00271     smart_dir_node mediaservers = node->addSubDir(tr("Media Servers"));
00272     mediaservers->setPathRoot();
00273 
00274     m_lock.lock();
00275     QMutableHashIterator<QString,MediaServer*> it(m_servers);
00276     while (it.hasNext())
00277     {
00278         it.next();
00279         if (!it.value()->m_subscribed)
00280             continue;
00281 
00282         QString usn = it.key();
00283         GetServerContent(usn, it.value(), list, mediaservers.get());
00284     }
00285     m_lock.unlock();
00286 }
00287 
00288 bool UPNPScanner::GetMetadata(QVariant &data)
00289 {
00290     // we need a USN and objectID
00291     if (!data.canConvert(QVariant::StringList))
00292         return false;
00293 
00294     QStringList list = data.toStringList();
00295     if (list.size() != 2)
00296         return false;
00297 
00298     QString usn = list[0];
00299     QString object = list[1];
00300 
00301     m_lock.lock();
00302     bool valid = m_servers.contains(usn);
00303     if (valid)
00304     {
00305         MediaServerItem* item = m_servers[usn]->Find(object);
00306         valid = item ? !item->m_scanned : false;
00307     }
00308     m_lock.unlock();
00309     if (!valid)
00310         return false;
00311 
00312     MythEvent *me = new MythEvent("UPNP_BROWSEOBJECT", list);
00313     qApp->postEvent(this, me);
00314 
00315     int count = 0;
00316     bool found = false;
00317     LOG(VB_GENERAL, LOG_INFO, "START");
00318     while (!found && (count++ < 100)) // 10 seconds
00319     {
00320         usleep(100000);
00321         m_lock.lock();
00322         if (m_servers.contains(usn))
00323         {
00324             MediaServerItem *item = m_servers[usn]->Find(object);
00325             if (item)
00326             {
00327                 found = item->m_scanned;
00328             }
00329             else
00330             {
00331                 LOG(VB_GENERAL, LOG_INFO, QString("Item went away..."));
00332                 found = true;
00333             }
00334         }
00335         else
00336         {
00337             LOG(VB_GENERAL, LOG_INFO,
00338                 QString("Server went away while browsing."));
00339             found = true;
00340         }
00341         m_lock.unlock();
00342     }
00343     LOG(VB_GENERAL, LOG_INFO, "END");
00344     return true;
00345 }
00346 
00352 void UPNPScanner::GetServerContent(QString &usn,
00353                                    MediaServerItem *content,
00354                                    VideoMetadataListManager::metadata_list* list,
00355                                    meta_dir_node *node)
00356 {
00357     if (!content->m_scanned)
00358     {
00359         smart_dir_node subnode = node->addSubDir(content->m_name);
00360 
00361         QStringList data;
00362         data << usn;
00363         data << content->m_id;
00364         subnode->SetData(data);
00365 
00366         VideoMetadataListManager::VideoMetadataPtr item(new VideoMetadata(QString()));
00367         item->SetTitle(QString("Dummy"));
00368         list->push_back(item);
00369         subnode->addEntry(smart_meta_node(new meta_data_node(item.get())));
00370         return;
00371     }
00372 
00373     node->SetData(QVariant());
00374 
00375     if (content->m_url.isEmpty())
00376     {
00377         smart_dir_node container = node->addSubDir(content->m_name);
00378         QMutableMapIterator<QString,MediaServerItem> it(content->m_children);
00379         while (it.hasNext())
00380         {
00381             it.next();
00382             GetServerContent(usn, &it.value(), list, container.get());
00383         }
00384         return;
00385     }
00386 
00387     VideoMetadataListManager::VideoMetadataPtr item(new VideoMetadata(content->m_url));
00388     item->SetTitle(content->m_name);
00389     list->push_back(item);
00390     node->addEntry(smart_meta_node(new meta_data_node(item.get())));
00391 }
00392 
00398 QMap<QString,QString> UPNPScanner::ServerList(void)
00399 {
00400     QMap<QString,QString> servers;
00401     m_lock.lock();
00402     QHashIterator<QString,MediaServer*> it(m_servers);
00403     while (it.hasNext())
00404     {
00405         it.next();
00406         servers.insert(it.key(), it.value()->m_friendlyName);
00407     }
00408     m_lock.unlock();
00409     return servers;
00410 }
00411 
00417 void UPNPScanner::Start()
00418 {
00419     m_lock.lock();
00420 
00421     // create our network handler
00422     m_network = new QNetworkAccessManager();
00423     connect(m_network, SIGNAL(finished(QNetworkReply*)),
00424             this, SLOT(replyFinished(QNetworkReply*)));
00425 
00426     // listen for SSDP updates
00427     SSDP::Instance()->AddListener(this);
00428 
00429     // listen for subscriptions and events
00430     if (m_subscription)
00431         m_subscription->addListener(this);
00432 
00433     // create our update timer (driven by AddServer and ParseDescription)
00434     m_updateTimer = new QTimer(this);
00435     m_updateTimer->setSingleShot(true);
00436     connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(Update()));
00437 
00438     // create our watchdog timer (checks for stale servers)
00439     m_watchdogTimer = new QTimer(this);
00440     connect(m_watchdogTimer, SIGNAL(timeout()), this, SLOT(CheckStatus()));
00441     m_watchdogTimer->start(1000 * 10); // every 10s
00442 
00443     // avoid connecting to the master backend
00444     m_masterHost = gCoreContext->GetSetting("MasterServerIP");
00445     m_masterPort = gCoreContext->GetSettingOnHost("BackendStatusPort",
00446                                              m_masterHost, "6544").toInt();
00447 
00448     m_lock.unlock();
00449     LOG(VB_GENERAL, LOG_INFO, LOC + "Started");
00450 }
00451 
00456 void UPNPScanner::Stop(void)
00457 {
00458     m_lock.lock();
00459 
00460     // stop listening
00461     SSDP::Instance()->RemoveListener(this);
00462     if (m_subscription)
00463         m_subscription->removeListener(this);
00464 
00465     // disable updates
00466     if (m_updateTimer)
00467         m_updateTimer->stop();
00468     if (m_watchdogTimer)
00469         m_watchdogTimer->stop();
00470 
00471     // cleanup our servers and subscriptions
00472     QHashIterator<QString,MediaServer*> it(m_servers);
00473     while (it.hasNext())
00474     {
00475         it.next();
00476         if (m_subscription && it.value()->m_subscribed)
00477             m_subscription->Unsubscribe(it.key());
00478         if (it.value()->m_renewalTimerId)
00479             killTimer(it.value()->m_renewalTimerId);
00480         delete it.value();
00481     }
00482     m_servers.clear();
00483 
00484     // cleanup the network
00485     foreach (QNetworkReply *reply, m_descriptionRequests)
00486     {
00487         reply->abort();
00488         delete reply;
00489     }
00490     m_descriptionRequests.clear();
00491     foreach (QNetworkReply *reply, m_browseRequests)
00492     {
00493         reply->abort();
00494         delete reply;
00495     }
00496     m_browseRequests.clear();
00497     delete m_network;
00498     m_network = NULL;
00499 
00500     // delete the timers
00501     delete m_updateTimer;
00502     delete m_watchdogTimer;
00503     m_updateTimer   = NULL;
00504     m_watchdogTimer = NULL;
00505 
00506     m_lock.unlock();
00507     LOG(VB_GENERAL, LOG_INFO, LOC + "Finished");
00508 }
00509 
00515 void UPNPScanner::Update(void)
00516 {
00517     // decide which servers still need to be checked
00518     m_lock.lock();
00519     if (m_servers.isEmpty())
00520     {
00521         m_lock.unlock();
00522         return;
00523     }
00524 
00525     // if our network queue is full, then we may need to come back later
00526     bool reschedule = false;
00527 
00528     QHashIterator<QString,MediaServer*> it(m_servers);
00529     while (it.hasNext())
00530     {
00531         it.next();
00532         if ((it.value()->m_connectionAttempts < MAX_ATTEMPTS) &&
00533             (it.value()->m_controlURL.isEmpty()))
00534         {
00535             bool sent = false;
00536             QUrl url = it.value()->m_URL;
00537             if (!m_descriptionRequests.contains(url) &&
00538                 (m_descriptionRequests.size() < MAX_REQUESTS) &&
00539                 url.isValid())
00540             {
00541                 QNetworkReply *reply = m_network->get(QNetworkRequest(url));
00542                 if (reply)
00543                 {
00544                     sent = true;
00545                     m_descriptionRequests.insert(url, reply);
00546                     it.value()->m_connectionAttempts++;
00547                 }
00548             }
00549             if (!sent)
00550                 reschedule = true;
00551         }
00552     }
00553 
00554     if (reschedule)
00555         ScheduleUpdate();
00556     m_lock.unlock();
00557 }
00558 
00563 void UPNPScanner::CheckStatus(void)
00564 {
00565     // FIXME
00566     // Remove stale servers - the SSDP cache code does not send out removal
00567     // notifications for expired (rather than explicitly closed) connections
00568     m_lock.lock();
00569     QMutableHashIterator<QString,MediaServer*> it(m_servers);
00570     while (it.hasNext())
00571     {
00572         it.next();
00573         if (!SSDP::Find("urn:schemas-upnp-org:device:MediaServer:1", it.key()))
00574         {
00575             LOG(VB_UPNP, LOG_INFO, LOC +
00576                 QString("%1 no longer in SSDP cache. Removing")
00577                     .arg(it.value()->m_URL.toString()));
00578             MediaServer* last = it.value();
00579             it.remove();
00580             delete last;
00581         }
00582     }
00583     m_lock.unlock();
00584 }
00585 
00591 void UPNPScanner::replyFinished(QNetworkReply *reply)
00592 {
00593     if (!reply)
00594         return;
00595 
00596     QUrl url   = reply->url();
00597     bool valid = reply->error() == QNetworkReply::NoError;
00598 
00599     if (!valid)
00600     {
00601         LOG(VB_UPNP, LOG_ERR, LOC +
00602             QString("Network request for '%1' returned error '%2'")
00603                 .arg(url.toString()).arg(reply->errorString()));
00604     }
00605 
00606     bool description = false;
00607     bool browse      = false;
00608 
00609     m_lock.lock();
00610     if (m_descriptionRequests.contains(url, reply))
00611     {
00612         m_descriptionRequests.remove(url, reply);
00613         description = true;
00614     }
00615     else if (m_browseRequests.contains(url, reply))
00616     {
00617         m_browseRequests.remove(url, reply);
00618         browse = true;
00619     }
00620     m_lock.unlock();
00621 
00622     if (browse && valid)
00623     {
00624         ParseBrowse(url, reply);
00625         if (m_fullscan)
00626             BrowseNextContainer();
00627     }
00628     else if (description)
00629     {
00630         if (!valid || (valid && !ParseDescription(url, reply)))
00631         {
00632             // if there will be no more attempts, update the logs
00633             CheckFailure(url);
00634             // try again
00635             ScheduleUpdate();
00636         }
00637     }
00638     else
00639         LOG(VB_UPNP, LOG_ERR, LOC + "Received unknown reply");
00640 
00641     reply->deleteLater();
00642 }
00643 
00648 void UPNPScanner::customEvent(QEvent *event)
00649 {
00650     if ((MythEvent::Type)(event->type()) != MythEvent::MythEventMessage)
00651         return;
00652 
00653     // UPnP events
00654     MythEvent *me  = (MythEvent *)event;
00655     QString    ev  = me->Message();
00656 
00657     if (ev == "UPNP_STARTSCAN")
00658     {
00659         BrowseNextContainer();
00660         return;
00661     }
00662     else if (ev == "UPNP_BROWSEOBJECT")
00663     {
00664         if (me->ExtraDataCount() == 2)
00665         {
00666             QUrl url;
00667             QString usn = me->ExtraData(0);
00668             QString objectid = me->ExtraData(1);
00669             m_lock.lock();
00670             if (m_servers.contains(usn))
00671             {
00672                 url = m_servers[usn]->m_controlURL;
00673                 LOG(VB_GENERAL, LOG_INFO, QString("UPNP_BROWSEOBJECT: %1->%2")
00674                     .arg(m_servers[usn]->m_friendlyName).arg(objectid));
00675             }
00676             m_lock.unlock();
00677             if (!url.isEmpty())
00678                 SendBrowseRequest(url, objectid);
00679         }
00680         return;
00681     }
00682     else if (ev == "UPNP_EVENT")
00683     {
00684         MythInfoMapEvent *info = (MythInfoMapEvent*)event;
00685         if (!info)
00686             return;
00687         if (!info->InfoMap())
00688             return;
00689 
00690         QString usn = info->InfoMap()->value("usn");
00691         QString id  = info->InfoMap()->value("SystemUpdateID");
00692         if (usn.isEmpty() || id.isEmpty())
00693             return;
00694 
00695         m_lock.lock();
00696         if (m_servers.contains(usn))
00697         {
00698             int newid = id.toInt();
00699             if (m_servers[usn]->m_systemUpdateID != newid)
00700             {
00701                 m_scanComplete &= m_servers[usn]->ResetContent(newid);
00702                 LOG(VB_GENERAL, LOG_INFO, LOC +
00703                     QString("New SystemUpdateID '%1' for %2").arg(id).arg(usn));
00704                 Debug();
00705             }
00706         }
00707         m_lock.unlock();
00708         return;
00709     }
00710 
00711     // process SSDP cache updates
00712     QString    uri = me->ExtraDataCount() > 0 ? me->ExtraData(0) : QString();
00713     QString    usn = me->ExtraDataCount() > 1 ? me->ExtraData(1) : QString();
00714 
00715     if (uri == "urn:schemas-upnp-org:device:MediaServer:1")
00716     {
00717         QString url = (ev == "SSDP_ADD") ? me->ExtraData(2) : QString();
00718         AddServer(usn, url);
00719     }
00720 }
00721 
00726 void UPNPScanner::timerEvent(QTimerEvent * event)
00727 {
00728     int id = event->timerId();
00729     if (id)
00730         killTimer(id);
00731 
00732     int timeout = 0;
00733     QString usn;
00734 
00735     m_lock.lock();
00736     QHashIterator<QString,MediaServer*> it(m_servers);
00737     while (it.hasNext())
00738     {
00739         it.next();
00740         if (it.value()->m_renewalTimerId == id)
00741         {
00742             it.value()->m_renewalTimerId = 0;
00743             usn = it.key();
00744             if (m_subscription)
00745                 timeout = m_subscription->Renew(usn);
00746         }
00747     }
00748     m_lock.unlock();
00749 
00750     if (timeout > 0)
00751     {
00752         ScheduleRenewal(usn, timeout);
00753         LOG(VB_GENERAL, LOG_INFO, LOC +
00754             QString("Re-subscribed for %1 seconds to %2")
00755                 .arg(timeout).arg(usn));
00756     }
00757 }
00758 
00762 void UPNPScanner::ScheduleUpdate(void)
00763 {
00764     m_lock.lock();
00765     if (m_updateTimer && !m_updateTimer->isActive())
00766         m_updateTimer->start(200);
00767     m_lock.unlock();
00768 }
00769 
00774 void UPNPScanner::CheckFailure(const QUrl &url)
00775 {
00776     m_lock.lock();
00777     QHashIterator<QString,MediaServer*> it(m_servers);
00778     while (it.hasNext())
00779     {
00780         it.next();
00781         if (it.value()->m_URL == url &&
00782             it.value()->m_connectionAttempts == MAX_ATTEMPTS)
00783         {
00784             Debug();
00785             break;
00786         }
00787     }
00788     m_lock.unlock();
00789 }
00790 
00794 void UPNPScanner::Debug(void)
00795 {
00796     m_lock.lock();
00797     LOG(VB_UPNP, LOG_INFO, LOC + QString("%1 media servers discovered:")
00798                                    .arg(m_servers.size()));
00799     QHashIterator<QString,MediaServer*> it(m_servers);
00800     while (it.hasNext())
00801     {
00802         it.next();
00803         QString status = "Probing";
00804         if (it.value()->m_controlURL.toString().isEmpty())
00805         {
00806             if (it.value()->m_connectionAttempts >= MAX_ATTEMPTS)
00807                 status = "Failed";
00808         }
00809         else
00810             status = "Yes";
00811         LOG(VB_UPNP, LOG_INFO, LOC +
00812             QString("'%1' Connected: %2 Subscribed: %3 SystemUpdateID: "
00813                     "%4 timerId: %5")
00814                 .arg(it.value()->m_friendlyName).arg(status)
00815                 .arg(it.value()->m_subscribed ? "Yes" : "No")
00816                 .arg(it.value()->m_systemUpdateID)
00817                 .arg(it.value()->m_renewalTimerId));
00818     }
00819     m_lock.unlock();
00820 }
00821 
00830 void UPNPScanner::BrowseNextContainer(void)
00831 {
00832     QMutexLocker locker(&m_lock);
00833 
00834     QHashIterator<QString,MediaServer*> it(m_servers);
00835     bool complete = true;
00836     while (it.hasNext())
00837     {
00838         it.next();
00839         if (it.value()->m_subscribed)
00840         {
00841             // limit browse requests to one active per server
00842             if (m_browseRequests.contains(it.value()->m_controlURL))
00843             {
00844                 complete = false;
00845                 continue;
00846             }
00847 
00848             QString next = it.value()->NextUnbrowsed();
00849             if (!next.isEmpty())
00850             {
00851                 complete = false;
00852                 SendBrowseRequest(it.value()->m_controlURL, next);
00853                 continue;
00854             }
00855 
00856             LOG(VB_UPNP, LOG_INFO, LOC + QString("Scan completed for %1")
00857                 .arg(it.value()->m_friendlyName));
00858         }
00859     }
00860 
00861     if (complete)
00862     {
00863         LOG(VB_GENERAL, LOG_INFO, LOC +
00864             QString("Media Server scan is complete."));
00865         m_scanComplete = true;
00866         m_fullscan = false;
00867     }
00868 }
00869 
00875 void UPNPScanner::SendBrowseRequest(const QUrl &url, const QString &objectid)
00876 {
00877     QNetworkRequest req = QNetworkRequest(url);
00878     req.setRawHeader("CONTENT-TYPE", "text/xml; charset=\"utf-8\"");
00879     req.setRawHeader("SOAPACTION",
00880                   "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"");
00881 #if 0
00882     req.setRawHeader("MAN", "\"http://schemasxmlsoap.org/soap/envelope/\"");
00883     req.setRawHeader("01-SOAPACTION",
00884                   "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"");
00885 #endif
00886 
00887     QByteArray body;
00888     QTextStream data(&body);
00889     data.setCodec(QTextCodec::codecForName("UTF-8"));
00890     data << "<?xml version=\"1.0\"?>\r\n";
00891     data << "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n";
00892     data << "  <s:Body>\r\n";
00893     data << "    <u:Browse xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">\r\n";
00894     data << "      <ObjectID>" << objectid.toUtf8() << "</ObjectID>\r\n";
00895     data << "      <BrowseFlag>BrowseDirectChildren</BrowseFlag>\r\n";
00896     data << "      <Filter>*</Filter>\r\n";
00897     data << "      <StartingIndex>0</StartingIndex>\r\n";
00898     data << "      <RequestedCount>0</RequestedCount>\r\n";
00899     data << "      <SortCriteria></SortCriteria>\r\n";
00900     data << "    </u:Browse>\r\n";
00901     data << "  </s:Body>\r\n";
00902     data << "</s:Envelope>\r\n";
00903     data.flush();
00904 
00905     m_lock.lock();
00906     QNetworkReply *reply = m_network->post(req, body);
00907     if (reply)
00908         m_browseRequests.insert(url, reply);
00909     m_lock.unlock();
00910 }
00911 
00917 void UPNPScanner::AddServer(const QString &usn, const QString &url)
00918 {
00919     if (url.isEmpty())
00920     {
00921         RemoveServer(usn);
00922         return;
00923     }
00924 
00925     // sometimes initialisation is too early and m_masterHost is empty
00926     if (m_masterHost.isEmpty())
00927     {
00928         m_masterHost = gCoreContext->GetSetting("MasterServerIP");
00929         m_masterPort = gCoreContext->GetSettingOnHost("BackendStatusPort",
00930                                                   m_masterHost, "6544").toInt();
00931     }
00932 
00933     QUrl qurl(url);
00934     if (qurl.host() == m_masterHost && qurl.port() == m_masterPort)
00935     {
00936         LOG(VB_UPNP, LOG_INFO, LOC + "Ignoring master backend.");
00937         return;
00938     }
00939 
00940     m_lock.lock();
00941     if (!m_servers.contains(usn))
00942     {
00943         m_servers.insert(usn, new MediaServer(url));
00944         LOG(VB_GENERAL, LOG_INFO, LOC + QString("Adding: %1").arg(usn));
00945         ScheduleUpdate();
00946     }
00947     m_lock.unlock();
00948 }
00949 
00953 void UPNPScanner::RemoveServer(const QString &usn)
00954 {
00955     m_lock.lock();
00956     if (m_servers.contains(usn))
00957     {
00958         LOG(VB_GENERAL, LOG_INFO, LOC + QString("Removing: %1").arg(usn));
00959         MediaServer* old = m_servers[usn];
00960         if (old->m_renewalTimerId)
00961             killTimer(old->m_renewalTimerId);
00962         m_servers.remove(usn);
00963         delete old;
00964         if (m_subscription)
00965             m_subscription->Remove(usn);
00966     }
00967     m_lock.unlock();
00968 
00969     Debug();
00970 }
00971 
00976 void UPNPScanner::ScheduleRenewal(const QString &usn, int timeout)
00977 {
00978     // sanitise the timeout
00979     int renew = timeout - 10;
00980     if (renew < 10)
00981         renew = 10;
00982     if (renew > 43200)
00983         renew = 43200;
00984 
00985     m_lock.lock();
00986     if (m_servers.contains(usn))
00987         m_servers[usn]->m_renewalTimerId = startTimer(renew * 1000);
00988     m_lock.unlock();
00989 }
00990 
00995 void UPNPScanner::ParseBrowse(const QUrl &url, QNetworkReply *reply)
00996 {
00997     QByteArray data = reply->readAll();
00998     if (data.isEmpty())
00999         return;
01000 
01001     // Open the response for parsing
01002     QDomDocument *parent = new QDomDocument();
01003     QString errorMessage;
01004     int errorLine   = 0;
01005     int errorColumn = 0;
01006     if (!parent->setContent(data, false, &errorMessage, &errorLine,
01007                             &errorColumn))
01008     {
01009         LOG(VB_GENERAL, LOG_ERR, LOC +
01010             QString("DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
01011                 .arg(errorLine).arg(errorColumn).arg(errorMessage));
01012         delete parent;
01013         return;
01014     }
01015 
01016     LOG(VB_UPNP, LOG_INFO, "\n\n" + parent->toString(4) + "\n\n");
01017 
01018     // pull out the actual result
01019     QDomDocument *result = NULL;
01020     uint num      = 0;
01021     uint total    = 0;
01022     uint updateid = 0;
01023     QDomElement docElem = parent->documentElement();
01024     QDomNode n = docElem.firstChild();
01025     if (!n.isNull())
01026         result = FindResult(n, num, total, updateid);
01027     delete parent;
01028 
01029     if (!result || num < 1 || total < 1)
01030     {
01031         LOG(VB_GENERAL, LOG_ERR, LOC +
01032             QString("Failed to find result for %1") .arg(url.toString()));
01033         return;
01034     }
01035 
01036     // determine the 'server' which requested the browse
01037     m_lock.lock();
01038 
01039     MediaServer* server = NULL;
01040     QHashIterator<QString,MediaServer*> it(m_servers);
01041     while (it.hasNext())
01042     {
01043         it.next();
01044         if (url == it.value()->m_controlURL)
01045         {
01046             server = it.value();
01047             break;
01048         }
01049     }
01050 
01051     // discard unmatched responses
01052     if (!server)
01053     {
01054         m_lock.unlock();
01055         LOG(VB_GENERAL, LOG_ERR, LOC +
01056             QString("Received unknown response for %1").arg(url.toString()));
01057         return;
01058     }
01059 
01060     // check the update ID
01061     if (server->m_systemUpdateID != (int)updateid)
01062     {
01063         // if this is not the root container, this browse will now fail
01064         // as the appropriate parentID will not be found
01065         LOG(VB_GENERAL, LOG_ERR, LOC +
01066             QString("%1 updateID changed during browse (old %2 new %3)")
01067                 .arg(server->m_friendlyName).arg(server->m_systemUpdateID)
01068                 .arg(updateid));
01069         m_scanComplete &= server->ResetContent(updateid);
01070         Debug();
01071     }
01072 
01073     // find containers (directories) and actual items and add them and reset
01074     // the parent when we have found the first item
01075     bool reset = true;
01076     docElem = result->documentElement();
01077     n = docElem.firstChild();
01078     while (!n.isNull())
01079     {
01080         FindItems(n, *server, reset);
01081         n = n.nextSibling();
01082     }
01083     delete result;
01084 
01085     m_lock.unlock();
01086 }
01087 
01088 void UPNPScanner::FindItems(const QDomNode &n, MediaServerItem &content,
01089                             bool &resetparent)
01090 {
01091     QDomElement node = n.toElement();
01092     if (node.isNull())
01093         return;
01094 
01095     if (node.tagName() == "container")
01096     {
01097         QString title = "ERROR";
01098         QDomNode next = node.firstChild();
01099         while (!next.isNull())
01100         {
01101             QDomElement container = next.toElement();
01102             if (!container.isNull() && container.tagName() == "title")
01103                 title = container.text();
01104             next = next.nextSibling();
01105         }
01106 
01107         QString thisid   = node.attribute("id", "ERROR");
01108         QString parentid = node.attribute("parentID", "ERROR");
01109         MediaServerItem container =
01110             MediaServerItem(thisid, parentid, title, QString());
01111         MediaServerItem *parent = content.Find(parentid);
01112         if (parent)
01113         {
01114             if (resetparent)
01115             {
01116                 parent->Reset();
01117                 resetparent = false;
01118             }
01119             parent->m_scanned = true;
01120             parent->Add(container);
01121         }
01122         return;
01123     }
01124 
01125     if (node.tagName() == "item")
01126     {
01127         QString title = "ERROR";
01128         QString url = "ERROR";
01129         QDomNode next = node.firstChild();
01130         while (!next.isNull())
01131         {
01132             QDomElement item = next.toElement();
01133             if (!item.isNull())
01134             {
01135                 if(item.tagName() == "res")
01136                     url = item.text();
01137                 if(item.tagName() == "title")
01138                     title = item.text();
01139             }
01140             next = next.nextSibling();
01141         }
01142 
01143         QString thisid   = node.attribute("id", "ERROR");
01144         QString parentid = node.attribute("parentID", "ERROR");
01145         MediaServerItem item =
01146                 MediaServerItem(thisid, parentid, title, url);
01147         item.m_scanned = true;
01148         MediaServerItem *parent = content.Find(parentid);
01149         if (parent)
01150         {
01151             if (resetparent)
01152             {
01153                 parent->Reset();
01154                 resetparent = false;
01155             }
01156             parent->m_scanned = true;
01157             parent->Add(item);
01158         }
01159         return;
01160     }
01161 
01162     QDomNode next = node.firstChild();
01163     while (!next.isNull())
01164     {
01165         FindItems(next, content, resetparent);
01166         next = next.nextSibling();
01167     }
01168 }
01169 
01170 QDomDocument* UPNPScanner::FindResult(const QDomNode &n, uint &num,
01171                                       uint &total, uint &updateid)
01172 {
01173     QDomDocument *result = NULL;
01174     QDomElement node = n.toElement();
01175     if (node.isNull())
01176         return NULL;
01177 
01178     if (node.tagName() == "NumberReturned")
01179         num = node.text().toUInt();
01180     if (node.tagName() == "TotalMatches")
01181         total = node.text().toUInt();
01182     if (node.tagName() == "UpdateID")
01183         updateid = node.text().toUInt();
01184     if (node.tagName() == "Result" && !result)
01185     {
01186         QString errorMessage;
01187         int errorLine   = 0;
01188         int errorColumn = 0;
01189         result = new QDomDocument();
01190         if (!result->setContent(node.text(), true, &errorMessage, &errorLine, &errorColumn))
01191         {
01192             LOG(VB_GENERAL, LOG_ERR, LOC +
01193                 QString("DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
01194                     .arg(errorLine).arg(errorColumn).arg(errorMessage));
01195             delete result;
01196             result = NULL;
01197         }
01198     }
01199 
01200     QDomNode next = node.firstChild();
01201     while (!next.isNull())
01202     {
01203         QDomDocument *res = FindResult(next, num, total, updateid);
01204         if (res)
01205             result = res;
01206         next = next.nextSibling();
01207     }
01208     return result;
01209 }
01210 
01215 bool UPNPScanner::ParseDescription(const QUrl &url, QNetworkReply *reply)
01216 {
01217     if (url.isEmpty() || !reply)
01218         return false;
01219 
01220     QByteArray data = reply->readAll();
01221     if (data.isEmpty())
01222     {
01223         LOG(VB_GENERAL, LOG_ERR, LOC +
01224             QString("%1 returned an empty device description.")
01225                 .arg(url.toString()));
01226         return false;
01227     }
01228 
01229     // parse the device description
01230     QString controlURL = QString();
01231     QString eventURL   = QString();
01232     QString friendlyName = QString("Unknown");
01233     QString URLBase = QString();
01234 
01235     QDomDocument doc;
01236     QString errorMessage;
01237     int errorLine   = 0;
01238     int errorColumn = 0;
01239     if (!doc.setContent(data, false, &errorMessage, &errorLine, &errorColumn))
01240     {
01241         LOG(VB_GENERAL, LOG_ERR, LOC +
01242             QString("Failed to parse device description from %1")
01243                 .arg(url.toString()));
01244         LOG(VB_GENERAL, LOG_ERR, LOC + QString("Line: %1 Col: %2 Error: '%3'")
01245             .arg(errorLine).arg(errorColumn).arg(errorMessage));
01246         return false;
01247     }
01248 
01249     QDomElement docElem = doc.documentElement();
01250     QDomNode n = docElem.firstChild();
01251     while (!n.isNull())
01252     {
01253         QDomElement e1 = n.toElement();
01254         if (!e1.isNull())
01255         {
01256             if(e1.tagName() == "device")
01257                 ParseDevice(e1, controlURL, eventURL, friendlyName);
01258             if (e1.tagName() == "URLBase")
01259                 URLBase = e1.text();
01260         }
01261         n = n.nextSibling();
01262     }
01263 
01264     if (controlURL.isEmpty())
01265     {
01266         LOG(VB_UPNP, LOG_ERR, LOC +
01267             QString("Failed to parse device description for %1")
01268                 .arg(url.toString()));
01269         return false;
01270     }
01271 
01272     // if no URLBase was provided, use the known url
01273     if (URLBase.isEmpty())
01274         URLBase = url.toString(QUrl::RemovePath | QUrl::RemoveFragment |
01275                                QUrl::RemoveQuery);
01276 
01277     // strip leading slashes off the controlURL
01278     while (!controlURL.isEmpty() && controlURL.left(1) == "/")
01279         controlURL = controlURL.mid(1);
01280 
01281     // strip leading slashes off the eventURL
01282     //while (!eventURL.isEmpty() && eventURL.left(1) == "/")
01283     //    eventURL = eventURL.mid(1);
01284 
01285     // strip trailing slashes off URLBase
01286     while (!URLBase.isEmpty() && URLBase.right(1) == "/")
01287         URLBase = URLBase.mid(0, URLBase.size() - 1);
01288 
01289     controlURL = URLBase + "/" + controlURL;
01290     QString fulleventURL = URLBase + "/" + eventURL;
01291 
01292     LOG(VB_UPNP, LOG_INFO, LOC + QString("Control URL for %1 at %2")
01293             .arg(friendlyName).arg(controlURL));
01294     LOG(VB_UPNP, LOG_INFO, LOC + QString("Event URL for %1 at %2")
01295             .arg(friendlyName).arg(fulleventURL));
01296 
01297     // update the server details. If the server has gone away since the request
01298     // was posted, this will silently fail and we won't try again
01299     QString usn;
01300     QUrl qeventurl = QUrl(fulleventURL);
01301     int timeout = 0;
01302 
01303     m_lock.lock();
01304     QHashIterator<QString,MediaServer*> it(m_servers);
01305     while (it.hasNext())
01306     {
01307         it.next();
01308         if (it.value()->m_URL == url)
01309         {
01310             usn = it.key();
01311             QUrl qcontrolurl(controlURL);
01312             it.value()->m_controlURL   = qcontrolurl;
01313             it.value()->m_eventSubURL  = qeventurl;
01314             it.value()->m_eventSubPath = eventURL;
01315             it.value()->m_friendlyName = friendlyName;
01316             it.value()->m_name         = friendlyName;
01317             break;
01318         }
01319     }
01320 
01321     if (m_subscription && !usn.isEmpty())
01322     {
01323         timeout = m_subscription->Subscribe(usn, qeventurl, eventURL);
01324         m_servers[usn]->m_subscribed = (timeout > 0);
01325     }
01326     m_lock.unlock();
01327 
01328     if (timeout > 0)
01329     {
01330         LOG(VB_GENERAL, LOG_INFO, LOC +
01331             QString("Subscribed for %1 seconds to %2") .arg(timeout).arg(usn));
01332         ScheduleRenewal(usn, timeout);
01333         // we only scan servers we are subscribed to - and the scan is now
01334         // incomplete
01335         m_scanComplete = false;
01336     }
01337 
01338     Debug();
01339     return true;
01340 }
01341 
01342 
01343 void UPNPScanner::ParseDevice(QDomElement &element, QString &controlURL,
01344                               QString &eventURL, QString &friendlyName)
01345 {
01346     QDomNode dev = element.firstChild();
01347     while (!dev.isNull())
01348     {
01349         QDomElement e = dev.toElement();
01350         if (!e.isNull())
01351         {
01352             if (e.tagName() == "friendlyName")
01353                 friendlyName = e.text();
01354             if (e.tagName() == "serviceList")
01355                 ParseServiceList(e, controlURL, eventURL);
01356         }
01357         dev = dev.nextSibling();
01358     }
01359 }
01360 
01361 void UPNPScanner::ParseServiceList(QDomElement &element, QString &controlURL,
01362                                    QString &eventURL)
01363 {
01364     QDomNode list = element.firstChild();
01365     while (!list.isNull())
01366     {
01367         QDomElement e = list.toElement();
01368         if (!e.isNull())
01369             if (e.tagName() == "service")
01370                 ParseService(e, controlURL, eventURL);
01371         list = list.nextSibling();
01372     }
01373 }
01374 
01375 void UPNPScanner::ParseService(QDomElement &element, QString &controlURL,
01376                                QString &eventURL)
01377 {
01378     bool     iscds       = false;
01379     QString  control_url = QString();
01380     QString  event_url   = QString();
01381     QDomNode service     = element.firstChild();
01382 
01383     while (!service.isNull())
01384     {
01385         QDomElement e = service.toElement();
01386         if (!e.isNull())
01387         {
01388             if (e.tagName() == "serviceType")
01389                 iscds = (e.text() == "urn:schemas-upnp-org:service:ContentDirectory:1");
01390             if (e.tagName() == "controlURL")
01391                 control_url = e.text();
01392             if (e.tagName() == "eventSubURL")
01393                 event_url = e.text();
01394         }
01395         service = service.nextSibling();
01396     }
01397 
01398     if (iscds)
01399     {
01400         controlURL = control_url;
01401         eventURL   = event_url;
01402     }
01403 }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends