All pastes #2072527 Raw Edit

Miscellany

public text v1 · immutable
#2072527 ·published 2011-05-30 23:53 UTC
rendered paste body
diff --git a/mythtv/programs/mythfrontend/mediarenderer.cpp b/mythtv/programs/mythfrontend/mediarenderer.cpp
index 1a63bb7..296b843 100644
--- a/mythtv/programs/mythfrontend/mediarenderer.cpp
+++ b/mythtv/programs/mythfrontend/mediarenderer.cpp
@@ -12,6 +12,8 @@
 #include "mediarenderer.h"
 #include "mythfexml.h"
 #include "compat.h"
+#include "upnpsubscription.h"
+#include "upnpscanner.h"
 
 class MythFrontendStatus : public HttpServerExtension
 {
@@ -274,6 +276,14 @@ MediaRenderer::MediaRenderer()
 
         Start();
 
+        // create a subscription service
+        VERBOSE(VB_UPNP, "MediaRenderer::Registering MythFrontendSubscription service.");
+        UPNPSubscription *sub = new UPNPSubscription(m_pHttpServer->m_sSharePath, nPort);
+        m_pHttpServer->RegisterExtension(sub);
+
+        // create a scanner
+        UPNPScanner::Enable(true, sub);
+
         // ensure the frontend is aware of all backends (slave and master) and
         // other frontends
         SSDP::Instance()->PerformSearch("ssdp:all");
@@ -292,5 +302,6 @@ MediaRenderer::MediaRenderer()
 
 MediaRenderer::~MediaRenderer()
 {
+    UPNPScanner::Enable(false);
     delete m_pHttpServer;
 }
diff --git a/mythtv/programs/mythfrontend/mythfrontend.pro b/mythtv/programs/mythfrontend/mythfrontend.pro
index a2e4450..b74fd0b 100644
--- a/mythtv/programs/mythfrontend/mythfrontend.pro
+++ b/mythtv/programs/mythfrontend/mythfrontend.pro
@@ -24,7 +24,7 @@ HEADERS += playbackbox.h viewscheduled.h globalsettings.h audiogeneralsettings.h
 HEADERS += manualschedule.h programrecpriority.h channelrecpriority.h
 HEADERS += statusbox.h networkcontrol.h custompriority.h
 HEADERS += mediarenderer.h mythfexml.h playbackboxlistitem.h
-HEADERS += screenwizard.h exitprompt.h
+HEADERS += screenwizard.h exitprompt.h upnpscanner.h
 HEADERS += action.h mythcontrols.h keybindings.h keygrabber.h
 HEADERS += progfind.h guidegrid.h customedit.h
 HEADERS += schedulecommon.h progdetails.h scheduleeditor.h
@@ -48,7 +48,7 @@ SOURCES += proglist.cpp                 proglist_helpers.cpp
 SOURCES += playbackboxhelper.cpp        viewschedulediff.cpp
 SOURCES += themechooser.cpp             setupwizard_general.cpp
 SOURCES += setupwizard_audio.cpp        setupwizard_video.cpp
-SOURCES += grabbersettings.cpp
+SOURCES += grabbersettings.cpp upnpscanner.cpp
 
 macx {
     mac_bundle {
diff --git a/mythtv/programs/mythfrontend/upnpscanner.cpp b/mythtv/programs/mythfrontend/upnpscanner.cpp
new file mode 100644
index 0000000..74c76ab
--- /dev/null
+++ b/mythtv/programs/mythfrontend/upnpscanner.cpp
@@ -0,0 +1,854 @@
+#include <QTextCodec>
+
+#include "mythcorecontext.h"
+#include "mythverbose.h"
+#include "ssdp.h"
+#include "upnpscanner.h"
+
+#define LOC QString("UPnPScan: ")
+#define ERR QString("UPnPScan error: ")
+
+#define MAX_ATTEMPTS 5
+#define MAX_REQUESTS 1
+
+class MediaServer
+{
+  public:
+    MediaServer() { }
+    MediaServer(QUrl URL)
+     : m_URL(URL), m_connectionAttempts(0), m_controlURL(QUrl()),
+       m_eventSubURL(QUrl()), m_eventSubPath(QString()),
+       m_friendlyName(QString("Unknown")), m_subscribed(false),
+       m_renewalTimerId(0), m_systemUpdateID(-1)
+    {
+    }
+
+    QUrl    m_URL;
+    int     m_connectionAttempts;
+    QUrl    m_controlURL;
+    QUrl    m_eventSubURL;
+    QString m_eventSubPath;
+    QString m_friendlyName;
+    bool    m_subscribed;
+    int     m_renewalTimerId;
+    int     m_systemUpdateID;
+};
+
+UPNPScanner* UPNPScanner::gUPNPScanner        = NULL;
+bool         UPNPScanner::gUPNPScannerEnabled = false;
+QThread*     UPNPScanner::gUPNPScannerThread  = NULL;
+QMutex*      UPNPScanner::gUPNPScannerLock    = new QMutex(QMutex::Recursive);
+
+void UPNPScanner::Enable(bool enable, UPNPSubscription *sub)
+{
+    QMutexLocker locker(gUPNPScannerLock);
+    gUPNPScannerEnabled = enable;
+    Instance(sub);
+}
+
+UPNPScanner* UPNPScanner::Instance(UPNPSubscription *sub)
+{
+    QMutexLocker locker(gUPNPScannerLock);
+    if (!gUPNPScannerEnabled)
+    {
+        if (gUPNPScannerThread)
+        {
+            gUPNPScannerThread->quit();
+            gUPNPScannerThread->wait();
+        }
+        delete gUPNPScannerThread;
+        gUPNPScannerThread = NULL;
+        delete gUPNPScanner;
+        gUPNPScanner = NULL;
+        return NULL;
+    }
+
+    if (!gUPNPScannerThread)
+        gUPNPScannerThread = new QThread();
+    if (!gUPNPScanner)
+        gUPNPScanner = new UPNPScanner(sub);
+
+    if (!gUPNPScannerThread->isRunning())
+    {
+        gUPNPScanner->moveToThread(gUPNPScannerThread);
+        gUPNPScannerThread->connect(gUPNPScannerThread, SIGNAL(started()),
+                                    gUPNPScanner,       SLOT(Start()));
+        gUPNPScannerThread->start(QThread::LowestPriority);
+    }
+
+    return gUPNPScanner;
+}
+
+UPNPScanner::UPNPScanner(UPNPSubscription *sub)
+  : QObject(), m_subscription(sub), m_lock(QMutex::Recursive),
+    m_network(NULL), m_updateTimer(NULL), m_watchdogTimer(NULL),
+    m_masterHost(QString()), m_masterPort(0)
+{
+}
+
+UPNPScanner::~UPNPScanner()
+{
+    Stop();
+}
+
+void UPNPScanner::Start()
+{
+    m_lock.lock();
+
+    // create our network handler
+    m_network = new QNetworkAccessManager();
+    connect(m_network, SIGNAL(finished(QNetworkReply*)),
+            this, SLOT(replyFinished(QNetworkReply*)));
+
+    // listen for SSDP updates
+    SSDP::Instance()->AddListener(this);
+
+    // listen for subscriptions and events
+    if (m_subscription)
+        m_subscription->addListener(this);
+
+    // create our update timer (driven by AddServer and ParseDescription)
+    m_updateTimer = new QTimer(this);
+    m_updateTimer->setSingleShot(true);
+    connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(Update()));
+
+    // create our watchdog timer (checks for stale servers)
+    m_watchdogTimer = new QTimer(this);
+    connect(m_watchdogTimer, SIGNAL(timeout()), this, SLOT(CheckStatus()));
+    m_watchdogTimer->start(1000 * 10); // every 10s
+
+    // avoid connecting to the master backend
+    m_masterHost = gCoreContext->GetSetting("MasterServerIP");
+    m_masterPort = gCoreContext->GetSettingOnHost("BackendStatusPort",
+                                             m_masterHost, "6544").toInt();
+
+    m_lock.unlock();
+    VERBOSE(VB_GENERAL, LOC + "Started");
+}
+
+void UPNPScanner::Stop(void)
+{
+    m_lock.lock();
+
+    // stop listening
+    SSDP::Instance()->RemoveListener(this);
+    if (m_subscription)
+        m_subscription->removeListener(this);
+
+    // disable updates
+    if (m_updateTimer)
+        m_updateTimer->stop();
+    if (m_watchdogTimer)
+        m_watchdogTimer->stop();
+
+    // cleanup our servers and subscriptions
+    QHashIterator<QString,MediaServer*> it(m_servers);
+    while (it.hasNext())
+    {
+        it.next();
+        if (m_subscription && it.value()->m_subscribed)
+            m_subscription->Unsubscribe(it.key());
+        if (it.value()->m_renewalTimerId)
+            killTimer(it.value()->m_renewalTimerId);
+        delete it.value();
+    }
+    m_servers.clear();
+
+    // cleanup the network
+    foreach (QNetworkReply *reply, m_descriptionRequests)
+        reply->abort();
+    m_descriptionRequests.clear();
+    foreach (QNetworkReply *reply, m_browseRequests)
+        reply->abort();
+    m_browseRequests.clear();
+    delete m_network;
+    m_network = NULL;
+
+    // delete the timers
+    delete m_updateTimer;
+    delete m_watchdogTimer;
+    m_updateTimer   = NULL;
+    m_watchdogTimer = NULL;
+
+    m_lock.unlock();
+    VERBOSE(VB_GENERAL, LOC + "Finished");
+}
+
+void UPNPScanner::Update(void)
+{
+    VERBOSE(VB_IMPORTANT, "UPDATE");
+    // decide which servers still need to be checked
+    m_lock.lock();
+
+    // if our network queue is full, then we may need to come back later
+    bool reschedule = false;
+
+    if (m_servers.isEmpty())
+    {
+        m_lock.unlock();
+        return;
+    }
+
+    QHashIterator<QString,MediaServer*> it(m_servers);
+    while (it.hasNext())
+    {
+        it.next();
+        if ((it.value()->m_connectionAttempts < MAX_ATTEMPTS) &&
+            (it.value()->m_controlURL.isEmpty()))
+        {
+            bool sent = false;
+            QUrl url = it.value()->m_URL;
+            if (!m_descriptionRequests.contains(url) &&
+                (m_descriptionRequests.size() < MAX_REQUESTS) &&
+                url.isValid())
+            {
+                QNetworkReply *reply = m_network->get(QNetworkRequest(url));
+                if (reply)
+                {
+                    sent = true;
+                    m_descriptionRequests.insert(url, reply);
+                    it.value()->m_connectionAttempts++;
+                }
+            }
+            if (!sent)
+                reschedule = true;
+        }
+    }
+
+    if (reschedule)
+        ScheduleUpdate();
+    m_lock.unlock();
+}
+
+void UPNPScanner::CheckStatus(void)
+{
+    // FIXME
+    // Remove stale servers - the SSDP cache code does not send out removal
+    // notifications for expired (rather than explicitly closed) connections
+    m_lock.lock();
+    QMutableHashIterator<QString,MediaServer*> it(m_servers);
+    while (it.hasNext())
+    {
+        it.next();
+        if (!SSDP::Find("urn:schemas-upnp-org:device:MediaServer:1", it.key()))
+        {
+            VERBOSE(VB_UPNP, LOC + QString("%1 no longer in SSDP cache. Removing")
+                .arg(it.value()->m_URL.toString()));
+            MediaServer* last = it.value();
+            it.remove();
+            delete last;
+        }
+    }
+    m_lock.unlock();
+}
+
+void UPNPScanner::replyFinished(QNetworkReply *reply)
+{
+    if (!reply)
+        return;
+
+    QUrl url   = reply->url();
+    bool valid = reply->error() == QNetworkReply::NoError;
+
+    if (!valid)
+    {
+        VERBOSE(VB_UPNP, ERR +
+            QString("Network request for '%1' returned error '%2'")
+            .arg(url.toString()).arg(reply->errorString()));
+    }
+
+    bool description = false;
+    bool browse      = false;
+
+    m_lock.lock();
+    if (m_descriptionRequests.contains(url, reply))
+    {
+        m_descriptionRequests.remove(url, reply);
+        description = true;
+    }
+    else if (m_browseRequests.contains(url, reply))
+    {
+        m_browseRequests.remove(url, reply);
+        browse = true;
+    }
+    m_lock.unlock();
+
+    if (browse && valid)
+        ParseBrowse(url, reply);
+    else if (description)
+    {
+        if (!valid || (valid && !ParseDescription(url, reply)))
+        {
+            // if there will be no more attempts, update the logs
+            CheckFailure(url);
+            // try again
+            ScheduleUpdate();
+        }
+    }
+    else
+        VERBOSE(VB_UPNP, ERR + "Received unknown reply");
+
+    reply->deleteLater();
+}
+
+void UPNPScanner::customEvent(QEvent *event)
+{
+    if ((MythEvent::Type)(event->type()) != MythEvent::MythEventMessage)
+        return;
+
+    // UPnP events
+    MythEvent *me  = (MythEvent *)event;
+    QString    ev  = me->Message();
+    if (ev == "UPNP_EVENT")
+    {
+        MythInfoMapEvent *info = (MythInfoMapEvent*)event;
+        if (!info)
+            return;
+        if (!info->InfoMap())
+            return;
+
+        QString usn = info->InfoMap()->value("usn");
+        QString id  = info->InfoMap()->value("SystemUpdateID");
+        if (usn.isEmpty() || id.isEmpty())
+            return;
+
+        m_lock.lock();
+        if (m_servers.contains(usn))
+        {
+            m_servers[usn]->m_systemUpdateID = id.toInt();
+            VERBOSE(VB_GENERAL, LOC + QString("New SystemUpdateID '%1' for %2")
+                .arg(id).arg(usn));
+            Debug();
+        }
+        m_lock.unlock();
+        return;
+    }
+
+    // process SSDP cache updates
+    QString    uri = me->ExtraDataCount() > 0 ? me->ExtraData(0) : QString();
+    QString    usn = me->ExtraDataCount() > 1 ? me->ExtraData(1) : QString();
+
+    if (uri == "urn:schemas-upnp-org:device:MediaServer:1")
+    {
+        QString url = (ev == "SSDP_ADD") ? me->ExtraData(2) : QString();
+        AddServer(uri, usn, url);
+    }
+}
+
+void UPNPScanner::timerEvent(QTimerEvent * event)
+{
+    int id = event->timerId();
+    if (id)
+        killTimer(id);
+
+    int timeout = 0;
+    QString usn;
+
+    m_lock.lock();
+    QHashIterator<QString,MediaServer*> it(m_servers);
+    while (it.hasNext())
+    {
+        it.next();
+        if (it.value()->m_renewalTimerId == id)
+        {
+            it.value()->m_renewalTimerId = 0;
+            usn = it.key();
+            if (m_subscription)
+                timeout = m_subscription->Renew(usn);
+        }
+    }
+    m_lock.unlock();
+
+    if (timeout > 0)
+    {
+        ScheduleRenewal(usn, timeout);
+        VERBOSE(VB_GENERAL, LOC + QString("Re-subscribed for %1 seconds to %2")
+                .arg(timeout).arg(usn));
+    }
+}
+
+void UPNPScanner::ScheduleUpdate(void)
+{
+    m_lock.lock();
+    if (m_updateTimer && !m_updateTimer->isActive())
+        m_updateTimer->start(200);
+    m_lock.unlock();
+}
+
+void UPNPScanner::CheckFailure(const QUrl &url)
+{
+    m_lock.lock();
+    QHashIterator<QString,MediaServer*> it(m_servers);
+    while (it.hasNext())
+    {
+        it.next();
+        if (it.value()->m_URL == url &&
+            it.value()->m_connectionAttempts == MAX_ATTEMPTS)
+        {
+            Debug();
+            break;
+        }
+    }
+    m_lock.unlock();
+}
+
+void UPNPScanner::Debug(void)
+{
+    m_lock.lock();
+    VERBOSE(VB_UPNP, LOC + QString("%1 media servers discovered:")
+                                   .arg(m_servers.size()));
+    QHashIterator<QString,MediaServer*> it(m_servers);
+    while (it.hasNext())
+    {
+        it.next();
+        QString status = "Probing";
+        if (it.value()->m_controlURL.toString().isEmpty())
+        {
+            if (it.value()->m_connectionAttempts >= MAX_ATTEMPTS)
+                status = "Failed";
+        }
+        else
+            status = "Yes";
+        VERBOSE(VB_UPNP, LOC +
+            QString("'%1' Connected: %2 Subscribed: %3 SystemUpdateID: %4 timerId: %5")
+            .arg(it.value()->m_friendlyName).arg(status)
+            .arg(it.value()->m_subscribed ? "Yes" : "No")
+            .arg(it.value()->m_systemUpdateID)
+            .arg(it.value()->m_renewalTimerId));
+    }
+    m_lock.unlock();
+}
+
+void UPNPScanner::SendBrowseRequest(const QUrl &url, const QString &objectid)
+{
+    QNetworkRequest req = QNetworkRequest(url);
+    req.setRawHeader("CONTENT-TYPE", "text/xml; charset=\"utf-8\"");
+    req.setRawHeader("SOAPACTION", "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"");
+    //req.setRawHeader("MAN", "\"http://schemasxmlsoap.org/soap/envelope/\"");
+    //req.setRawHeader("01-SOAPACTION", "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"");
+
+    QByteArray body;
+    QTextStream data(&body);
+    data.setCodec(QTextCodec::codecForName("UTF-8"));
+    data << "<?xml version=\"1.0\"?>\r\n";
+    data << "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n";
+    data << "  <s:Body>\r\n";
+    data << "    <u:Browse xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">\r\n";
+    data << "      <ObjectID>" << objectid.toUtf8() << "</ObjectID>\r\n";
+    data << "      <BrowseFlag>BrowseDirectChildren</BrowseFlag>\r\n";
+    data << "      <Filter>*</Filter>\r\n";
+    data << "      <StartingIndex>0</StartingIndex>\r\n";
+    data << "      <RequestedCount>0</RequestedCount>\r\n";
+    data << "      <SortCriteria></SortCriteria>\r\n";
+    data << "    </u:Browse>\r\n";
+    data << "  </s:Body>\r\n";
+    data << "</s:Envelope>\r\n";
+    data.flush();
+
+    m_lock.lock();
+    QNetworkReply *reply = m_network->post(req, body);
+    if (reply)
+        m_browseRequests.insert(url, reply);
+    m_lock.unlock();
+}
+
+
+void UPNPScanner::AddServer(const QString &uri, const QString &usn,
+                            const QString &url)
+{
+    VERBOSE(VB_IMPORTANT, QString(">>>>>>>>>>>>>>>>> START %1").arg(url));
+    if (url.isEmpty())
+    {
+        RemoveServer(usn);
+        return;
+    }
+
+    // sometimes initialisation is too early and m_masterHost is empty
+    if (m_masterHost.isEmpty())
+    {
+        m_masterHost = gCoreContext->GetSetting("MasterServerIP");
+        m_masterPort = gCoreContext->GetSettingOnHost("BackendStatusPort",
+                                                  m_masterHost, "6544").toInt();
+    }
+
+    QUrl qurl(url);
+    if (qurl.host() == m_masterHost && qurl.port() == m_masterPort)
+    {
+        VERBOSE(VB_UPNP, LOC + "Ignoring master backend.");
+        return;
+    }
+
+    m_lock.lock();
+    if (!m_servers.contains(usn))
+    {
+        m_servers.insert(usn, new MediaServer(url));
+        VERBOSE(VB_GENERAL, LOC + QString("Adding: %1").arg(usn));
+        ScheduleUpdate();
+    }
+    m_lock.unlock();
+    VERBOSE(VB_IMPORTANT, QString("<<<<<<<<<<<<<< END %1").arg(url));
+}
+
+void UPNPScanner::RemoveServer(const QString &usn)
+{
+    m_lock.lock();
+    if (m_servers.contains(usn))
+    {
+        VERBOSE(VB_GENERAL, LOC + QString("Removing: %1").arg(usn));
+        MediaServer* old = m_servers[usn];
+        if (old->m_renewalTimerId)
+            killTimer(old->m_renewalTimerId);
+        m_servers.remove(usn);
+        delete old;
+        if (m_subscription)
+            m_subscription->Remove(usn);
+    }
+    m_lock.unlock();
+
+    Debug();
+}
+
+void UPNPScanner::ScheduleRenewal(const QString &usn, int timeout)
+{
+    // sanitise the timeout
+    int renew = timeout - 10;
+    if (renew < 10)
+        renew = 10;
+    if (renew > 43200)
+        renew = 43200;
+
+    m_lock.lock();
+    if (m_servers.contains(usn))
+        m_servers[usn]->m_renewalTimerId = startTimer(renew * 1000);
+    m_lock.unlock();
+}
+
+void UPNPScanner::ParseBrowse(const QUrl &url, QNetworkReply *reply)
+{
+    QByteArray data = reply->readAll();
+    if (data.isEmpty())
+        return;
+
+    QDomDocument *parent = new QDomDocument();
+    QString errorMessage;
+    int errorLine   = 0;
+    int errorColumn = 0;
+    if (!parent->setContent(data, false, &errorMessage, &errorLine, &errorColumn))
+    {
+        VERBOSE(VB_GENERAL, ERR + QString("DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
+            .arg(errorLine).arg(errorColumn).arg(errorMessage));
+        delete parent;
+        return;
+    }
+
+    VERBOSE(VB_UPNP|VB_EXTRA, "\n\n" + parent->toString(4));
+
+    QDomDocument *result = NULL;
+    uint num      = 0;
+    uint total    = 0;
+    uint updateid = 0;
+    QDomElement docElem = parent->documentElement();
+    QDomNode n = docElem.firstChild();
+    if (!n.isNull())
+        result = FindResult(n, num, total, updateid);
+    delete parent;
+
+    if (!result || num < 1 || total < 1)
+        return;
+
+    docElem = result->documentElement();
+    n = docElem.firstChild();
+    QList<QStringList> items;
+    while (!n.isNull())
+    {
+        FindItems(n, items);
+        n = n.nextSibling();
+    }
+
+    //foreach (QStringList list, items)
+    //    VERBOSE(VB_IMPORTANT, LOC + QString("%1 %2 %3 %4")
+    //        .arg(list[0]).arg(list[1]).arg(list[2]).arg(list[3]));
+    delete result;
+}
+
+void UPNPScanner::FindItems(QDomNode &n, QList<QStringList> &items)
+{
+    QDomElement node = n.toElement();
+    if (node.isNull())
+        return;
+
+    if (node.tagName() == "container")
+    {
+        QString title = "ERROR";
+        QDomNode next = node.firstChild();
+        while (!next.isNull())
+        {
+            QDomElement container = next.toElement();
+            if (!container.isNull() && container.tagName() == "title")
+                title = container.text();
+            next = next.nextSibling();
+        }
+        QStringList list;
+        list.append(node.attribute("id", "ERROR"));
+        list.append(node.attribute("parentID", "ERROR"));
+        list.append(title);
+        list.append(QString()); //url
+        items.append(list);
+        return;
+    }
+
+    if (node.tagName() == "item")
+    {
+        QString title = "ERROR";
+        QString url = "ERROR";
+        QDomNode next = node.firstChild();
+        while (!next.isNull())
+        {
+            QDomElement item = next.toElement();
+            if (!item.isNull())
+            {
+                if(item.tagName() == "res")
+                    url = item.text();
+                if(item.tagName() == "title")
+                    title = item.text();
+            }
+            next = next.nextSibling();
+        }
+        QStringList list;
+        list.append(node.attribute("id", "ERROR"));
+        list.append(node.attribute("parentID", "ERROR"));
+        list.append(title);
+        list.append(url);
+        items.append(list);
+        return;
+    }
+
+    QDomNode next = node.firstChild();
+    while (!next.isNull())
+    {
+        FindItems(next, items);
+        next = next.nextSibling();
+    }
+}
+
+QDomDocument* UPNPScanner::FindResult(QDomNode &n, uint &num, uint &total,
+                                      uint &updateid)
+{
+    QDomDocument *result = NULL;
+    QDomElement node = n.toElement();
+    if (node.isNull())
+        return NULL;
+
+    if (node.tagName() == "NumberReturned")
+        num = node.text().toUInt();
+    if (node.tagName() == "TotalMatches")
+        total = node.text().toUInt();
+    if (node.tagName() == "UpdateID")
+        updateid = node.text().toUInt();
+    if (node.tagName() == "Result" && !result)
+    {
+        QString errorMessage;
+        int errorLine   = 0;
+        int errorColumn = 0;
+        result = new QDomDocument();
+        if (!result->setContent(node.text(), true, &errorMessage, &errorLine, &errorColumn))
+        {
+            VERBOSE(VB_GENERAL, ERR + QString("DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
+                .arg(errorLine).arg(errorColumn).arg(errorMessage));
+            delete result;
+            result = NULL;
+        }
+    }
+
+    QDomNode next = node.firstChild();
+    while (!next.isNull())
+    {
+        QDomDocument *res = FindResult(next, num, total, updateid);
+        if (res)
+            result = res;
+        next = next.nextSibling();
+    }
+    return result;
+}
+
+bool UPNPScanner::ParseDescription(const QUrl &url, QNetworkReply *reply)
+{
+    if (url.isEmpty() || !reply)
+        return false;
+
+    QByteArray data = reply->readAll();
+    if (data.isEmpty())
+    {
+        VERBOSE(VB_GENERAL, ERR + QString("%1 returned an empty device description.")
+            .arg(url.toString()));
+        return false;
+    }
+
+    // parse the device description
+    QString controlURL = QString();
+    QString eventURL   = QString();
+    QString friendlyName = QString("Unknown");
+    QString URLBase = QString();
+
+    QDomDocument doc;
+    QString errorMessage;
+    int errorLine   = 0;
+    int errorColumn = 0;
+    if (!doc.setContent(data, false, &errorMessage, &errorLine, &errorColumn))
+    {
+        VERBOSE(VB_GENERAL, ERR + QString("Failed to parse device description from %1")
+            .arg(url.toString()));
+        VERBOSE(VB_GENERAL, ERR + QString("Line: %1 Col: %2 Error: '%3'")
+            .arg(errorLine).arg(errorColumn).arg(errorMessage));
+        return false;
+    }
+
+    QDomElement docElem = doc.documentElement();
+    QDomNode n = docElem.firstChild();
+    while (!n.isNull())
+    {
+        QDomElement e1 = n.toElement();
+        if (!e1.isNull())
+        {
+            if(e1.tagName() == "device")
+                ParseDevice(e1, controlURL, eventURL, friendlyName);
+            if (e1.tagName() == "URLBase")
+                URLBase = e1.text();
+        }
+        n = n.nextSibling();
+    }
+
+    if (controlURL.isEmpty())
+    {
+        VERBOSE(VB_UPNP, ERR + QString("Failed to parse device description for %1")
+            .arg(url.toString()));
+        return false;
+    }
+
+    // if no URLBase was provided, use the known url
+    if (URLBase.isEmpty())
+        URLBase = url.toString(QUrl::RemovePath  | QUrl::RemoveFragment | QUrl::RemoveQuery);
+
+    // strip leading slashes off the controlURL
+    while (!controlURL.isEmpty() && controlURL.left(1) == "/")
+        controlURL = controlURL.mid(1);
+
+    // strip leading slashes off the eventURL
+    //while (!eventURL.isEmpty() && eventURL.left(1) == "/")
+    //    eventURL = eventURL.mid(1);
+
+    // strip trailing slashes off URLBase
+    while (!URLBase.isEmpty() && URLBase.right(1) == "/")
+        URLBase = URLBase.mid(0, URLBase.size() - 1);
+
+    controlURL = URLBase + "/" + controlURL;
+    QString fulleventURL = URLBase + "/" + eventURL;
+
+    VERBOSE(VB_UPNP, LOC + QString("Control URL for %1 at %2")
+            .arg(friendlyName).arg(controlURL));
+    VERBOSE(VB_UPNP, LOC + QString("Event URL for %1 at %2")
+            .arg(friendlyName).arg(fulleventURL));
+
+    // update the server details. If the server has gone away since the request
+    // was posted, this will silently fail and we won't try again
+    QString usn;
+    QUrl qeventurl = QUrl(fulleventURL);
+    int timeout = 0;
+
+    m_lock.lock();
+    QHashIterator<QString,MediaServer*> it(m_servers);
+    while (it.hasNext())
+    {
+        it.next();
+        if (it.value()->m_URL == url)
+        {
+            usn = it.key();
+            QUrl qcontrolurl(controlURL);
+            it.value()->m_controlURL   = qcontrolurl;
+            it.value()->m_eventSubURL  = qeventurl;
+            it.value()->m_eventSubPath = eventURL;
+            it.value()->m_friendlyName = friendlyName;
+            break;
+        }
+    }
+
+    if (m_subscription && !usn.isEmpty())
+    {
+        timeout = m_subscription->Subscribe(usn, qeventurl, eventURL);
+        m_servers[usn]->m_subscribed = (timeout > 0);
+    }
+    m_lock.unlock();
+
+    if (timeout > 0)
+    {
+        VERBOSE(VB_GENERAL, LOC + QString("Subscribed for %1 seconds to %2")
+                .arg(timeout).arg(usn));
+        ScheduleRenewal(usn, timeout);
+    }
+
+    Debug();
+    return true;
+}
+
+
+void UPNPScanner::ParseDevice(QDomElement &element, QString &controlURL,
+                              QString &eventURL, QString &friendlyName)
+{
+    QDomNode dev = element.firstChild();
+    while (!dev.isNull())
+    {
+        QDomElement e = dev.toElement();
+        if (!e.isNull())
+        {
+            if (e.tagName() == "friendlyName")
+                friendlyName = e.text();
+            if (e.tagName() == "serviceList")
+                ParseServiceList(e, controlURL, eventURL);
+        }
+        dev = dev.nextSibling();
+    }
+}
+
+void UPNPScanner::ParseServiceList(QDomElement &element, QString &controlURL,
+                                   QString &eventURL)
+{
+    QDomNode list = element.firstChild();
+    while (!list.isNull())
+    {
+        QDomElement e = list.toElement();
+        if (!e.isNull())
+            if (e.tagName() == "service")
+                ParseService(e, controlURL, eventURL);
+        list = list.nextSibling();
+    }
+}
+
+void UPNPScanner::ParseService(QDomElement &element, QString &controlURL,
+                               QString &eventURL)
+{
+    bool     iscds       = false;
+    QString  control_url = QString();
+    QString  event_url   = QString();
+    QDomNode service     = element.firstChild();
+
+    while (!service.isNull())
+    {
+        QDomElement e = service.toElement();
+        if (!e.isNull())
+        {
+            if (e.tagName() == "serviceType")
+                iscds = (e.text() == "urn:schemas-upnp-org:service:ContentDirectory:1");
+            if (e.tagName() == "controlURL")
+                control_url = e.text();
+            if (e.tagName() == "eventSubURL")
+                event_url = e.text();
+        }
+        service = service.nextSibling();
+    }
+
+    if (iscds)
+    {
+        controlURL = control_url;
+        eventURL   = event_url;
+    }
+}
diff --git a/mythtv/programs/mythfrontend/upnpscanner.h b/mythtv/programs/mythfrontend/upnpscanner.h
new file mode 100644
index 0000000..16dba84
--- /dev/null
+++ b/mythtv/programs/mythfrontend/upnpscanner.h
@@ -0,0 +1,83 @@
+#ifndef UPNPSCANNER_H
+#define UPNPSCANNER_H
+
+#include <QThread>
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QMutex>
+#include <QTimer>
+#include <QDomDocument>
+
+#include "upnpexp.h"
+#include "upnpsubscription.h"
+
+class MediaServer;
+class UPNPSubscription;
+
+class UPNP_PUBLIC UPNPScanner : public QObject
+{
+   Q_OBJECT
+
+  public:
+    static void         Enable(bool enable, UPNPSubscription *sub = NULL);
+    static UPNPScanner* Instance(UPNPSubscription *sub = NULL);
+
+   ~UPNPScanner();
+
+  public slots:
+    void Start();
+    void Stop(void);
+    void Update(void);
+    void CheckStatus(void);
+    void replyFinished(QNetworkReply *reply);
+
+  protected:
+    virtual void customEvent(QEvent *event);
+    virtual void timerEvent(QTimerEvent * event);
+
+  private:
+    UPNPScanner(UPNPSubscription *sub);
+    void ScheduleUpdate(void);
+    void CheckFailure(const QUrl &url);
+    void Debug(void);
+    void SendBrowseRequest(const QUrl &url, const QString &objectid);
+    void AddServer(const QString &uri, const QString &usn, const QString &url);
+    void RemoveServer(const QString &usn);
+    void ScheduleRenewal(const QString &usn, int timeout);
+
+    // xml parsing of browse requests
+    void ParseBrowse(const QUrl &url, QNetworkReply *reply);
+    void FindItems(QDomNode &n, QList<QStringList> &items);
+    QDomDocument* FindResult(QDomNode &n, uint &num, uint &total, uint &updateid);
+
+    // xml parsing of device description
+    bool ParseDescription(const QUrl &url, QNetworkReply *reply);
+    void ParseDevice(QDomElement &element, QString &controlURL,
+                     QString &eventURL, QString &friendlyName);
+    void ParseServiceList(QDomElement &element, QString &controlURL,
+                          QString &eventURL);
+    void ParseService(QDomElement &element, QString &controlURL,
+                      QString &eventURL);
+
+  private:
+    static  UPNPScanner* gUPNPScanner;
+    static  bool         gUPNPScannerEnabled;
+    static  QThread*     gUPNPScannerThread;
+    static  QMutex*      gUPNPScannerLock;
+
+    UPNPSubscription *m_subscription;
+    QMutex  m_lock;
+    QHash<QString,MediaServer*> m_servers;
+    QNetworkAccessManager *m_network;
+    QMultiHash<QUrl, QNetworkReply*> m_descriptionRequests;
+    QMultiHash<QUrl, QNetworkReply*> m_browseRequests;
+
+    QTimer *m_updateTimer;
+    QTimer *m_watchdogTimer;
+
+    QString m_masterHost;
+    int     m_masterPort;
+};
+
+#endif // UPNPSCANNER_H