--- a/src/contentdirectory.cxx
+++ b/src/contentdirectory.cxx
@@ -23,13 +23,53 @@
 #include <iostream>
 #include <map>
 #include <utility>
+#include <unordered_map>
 
 #include "libupnpp/log.hxx"
 #include "libupnpp/soaphelp.hxx"
 #include "libupnpp/device/device.hxx"
 
+#include "pathut.h"
+#include "smallut.h"
+#include "upmpdutils.hxx"
+#include "cdplugins/cdplugin.hxx"
+#include "cdplugins/tidal.hxx"
+
 using namespace std;
 using namespace std::placeholders;
+
+extern string g_datadir;
+
+class ContentDirectory::Internal {
+public:
+    ~Internal() {
+	for (auto& it : plugins) {
+	    delete it.second;
+	}
+    }
+    CDPlugin *pluginFactory(const string& appname) {
+	LOGDEB("ContentDirectory::pluginFactory: for " << appname << endl);
+	if (!appname.compare("tidal")) {
+	    string pthcdplugs = path_cat(g_datadir, "cdplugins");
+	    return new Tidal({pthcdplugs, path_cat(pthcdplugs, appname)});
+	} else {
+	    return nullptr;
+	}
+    }
+    CDPlugin *pluginForApp(const string& appname) {
+	auto it = plugins.find(appname);
+	if (it != plugins.end()) {
+	    return it->second;
+	} else {
+	    CDPlugin *plug = pluginFactory(appname);
+	    if (plug) {
+		plugins[appname] = plug;
+	    }
+	    return plug;
+	}
+    }
+    unordered_map<string, CDPlugin *> plugins;
+};
 
 static const string
 sTpContentDirectory("urn:schemas-upnp-org:service:ContentDirectory:1");
@@ -37,7 +77,8 @@
 sIdContentDirectory("urn:upnp-org:serviceId:ContentDirectory");
 
 ContentDirectory::ContentDirectory(UPnPProvider::UpnpDevice *dev)
-    : UpnpService(sTpContentDirectory, sIdContentDirectory, dev)
+    : UpnpService(sTpContentDirectory, sIdContentDirectory, dev),
+      m(new Internal)
 {
     dev->addActionMapping(
         this, "GetSearchCapabilities",
@@ -56,6 +97,10 @@
         bind(&ContentDirectory::actSearch, this, _1, _2));
 }
 
+ContentDirectory::~ContentDirectory()
+{
+    delete m;
+}
 
 int ContentDirectory::actGetSearchCapabilities(const SoapIncoming& sc, SoapOutgoing& data)
 {
@@ -85,6 +130,35 @@
     std::string out_Id;
     data.addarg("Id", out_Id);
     return UPNP_E_SUCCESS;
+}
+
+static vector<UpSong> rootdir;
+void makerootdir()
+{
+    rootdir.push_back(UpSong::container("0$tidal", "0", "Tidal"));
+}
+
+// Returns totalmatches
+static size_t readroot(int offs, int cnt, vector<UpSong>& out)
+{
+    //LOGDEB("readroot: offs " << offs << " cnt " << cnt << endl);
+    if (rootdir.empty()) {
+	makerootdir();
+    }
+    out.clear();
+    if (offs < 0 || cnt <= 0) {
+	return rootdir.size();
+    }
+	
+    for (int i = 0; i < cnt; i++) {
+	if (size_t(offs + i) < rootdir.size()) {
+	    out.push_back(rootdir[offs + i]);
+	} else {
+	    break;
+	}
+    }
+    //LOGDEB("readroot: returning " << out.size() << " entries\n");
+    return rootdir.size();
 }
 
 int ContentDirectory::actBrowse(const SoapIncoming& sc, SoapOutgoing& data)
@@ -127,13 +201,125 @@
         return UPNP_E_INVALID_PARAM;
     }
 
-    LOGDEB("ContentDirectory::actBrowse: " << " ObjectID " << in_ObjectID << " BrowseFlag " << in_BrowseFlag << " Filter " << in_Filter << " StartingIndex " << in_StartingIndex << " RequestedCount " << in_RequestedCount << " SortCriteria " << in_SortCriteria << endl);
-
+    LOGDEB("ContentDirectory::actBrowse: " << " ObjectID " << in_ObjectID <<
+	   " BrowseFlag " << in_BrowseFlag << " Filter " << in_Filter <<
+	   " StartingIndex " << in_StartingIndex <<
+	   " RequestedCount " << in_RequestedCount <<
+	   " SortCriteria " << in_SortCriteria << endl);
+
+    vector<string> sortcrits;
+    stringToStrings(in_SortCriteria, sortcrits);
+
+    CDPlugin::BrowseFlag bf;
+    if (in_BrowseFlag.compare("BrowseMetadata")) {
+	bf = CDPlugin::BFMeta;
+    } else {
+	bf = CDPlugin::BFChildren;
+    }
     std::string out_Result;
     std::string out_NumberReturned;
     std::string out_TotalMatches;
     std::string out_UpdateID;
 
+    // Go fetch
+    vector<UpSong> entries;
+    size_t tot = 0;
+    if (!in_ObjectID.compare("0")) {
+	// Root directory: we do this ourselves
+	tot = readroot(in_StartingIndex, in_RequestedCount, entries);
+    } else {
+	// Pass off request to appropriate app, defined by 1st elt in id
+	string app;
+	string::size_type dol0 = in_ObjectID.find_first_of("$");
+	if (dol0 == string::npos) {
+	    LOGERR("ContentDirectory::actBrowse: bad id [" << in_ObjectID <<
+		   "]\n");
+	} else {
+	    string::size_type dol1 = in_ObjectID.find_first_of("$", dol0 + 1);
+	    app = in_ObjectID.substr(dol0 + 1, dol1 - dol0 -1);
+	}
+	LOGDEB("ContentDirectory::actBrowse: app: [" << app << "]\n");
+
+	// for now, app has better be tidal...
+	CDPlugin *plg = m->pluginForApp(app);
+	if (plg) {
+	    out_TotalMatches = plg->browse(in_ObjectID, in_StartingIndex,
+					   in_RequestedCount, entries,
+					   sortcrits, bf);
+	} else {
+	    LOGERR("ContentDirectory::Browse: unknown app: [" << app << "]\n");
+	}
+    }
+
+
+    // Process and send out result
+    out_NumberReturned = ulltodecstr(entries.size());
+    out_TotalMatches = ulltodecstr(tot);
+    out_UpdateID = "1";
+    out_Result = headDIDL();
+    for (unsigned int i = 0; i < entries.size(); i++) {
+	out_Result += entries[i].didl();
+    } 
+    out_Result += tailDIDL();
+    
+    data.addarg("Result", out_Result);
+    LOGDEB("ContentDirectory::actBrowse: result [" << out_Result << "]\n");
+    data.addarg("NumberReturned", out_NumberReturned);
+    data.addarg("TotalMatches", out_TotalMatches);
+    data.addarg("UpdateID", out_UpdateID);
+    return UPNP_E_SUCCESS;
+}
+
+int ContentDirectory::actSearch(const SoapIncoming& sc, SoapOutgoing& data)
+{
+    bool ok = false;
+    std::string in_ContainerID;
+    ok = sc.get("ContainerID", &in_ContainerID);
+    if (!ok) {
+        LOGERR("ContentDirectory::actSearch: no ContainerID in params\n");
+        return UPNP_E_INVALID_PARAM;
+    }
+    std::string in_SearchCriteria;
+    ok = sc.get("SearchCriteria", &in_SearchCriteria);
+    if (!ok) {
+        LOGERR("ContentDirectory::actSearch: no SearchCriteria in params\n");
+        return UPNP_E_INVALID_PARAM;
+    }
+    std::string in_Filter;
+    ok = sc.get("Filter", &in_Filter);
+    if (!ok) {
+        LOGERR("ContentDirectory::actSearch: no Filter in params\n");
+        return UPNP_E_INVALID_PARAM;
+    }
+    int in_StartingIndex;
+    ok = sc.get("StartingIndex", &in_StartingIndex);
+    if (!ok) {
+        LOGERR("ContentDirectory::actSearch: no StartingIndex in params\n");
+        return UPNP_E_INVALID_PARAM;
+    }
+    int in_RequestedCount;
+    ok = sc.get("RequestedCount", &in_RequestedCount);
+    if (!ok) {
+        LOGERR("ContentDirectory::actSearch: no RequestedCount in params\n");
+        return UPNP_E_INVALID_PARAM;
+    }
+    std::string in_SortCriteria;
+    ok = sc.get("SortCriteria", &in_SortCriteria);
+    if (!ok) {
+        LOGERR("ContentDirectory::actSearch: no SortCriteria in params\n");
+        return UPNP_E_INVALID_PARAM;
+    }
+
+    LOGDEB("ContentDirectory::actSearch: " << " ContainerID " << in_ContainerID << " SearchCriteria " << in_SearchCriteria << " Filter " << in_Filter << " StartingIndex " << in_StartingIndex << " RequestedCount " << in_RequestedCount << " SortCriteria " << in_SortCriteria << endl);
+
+    std::string out_Result;
+    std::string out_NumberReturned;
+    std::string out_TotalMatches;
+    std::string out_UpdateID;
+
+    out_NumberReturned = "0";
+    out_TotalMatches = "0";
+
     data.addarg("Result", out_Result);
     data.addarg("NumberReturned", out_NumberReturned);
     data.addarg("TotalMatches", out_TotalMatches);
@@ -141,61 +327,3 @@
     return UPNP_E_SUCCESS;
 }
 
-int ContentDirectory::actSearch(const SoapIncoming& sc, SoapOutgoing& data)
-{
-    bool ok = false;
-    std::string in_ContainerID;
-    ok = sc.get("ContainerID", &in_ContainerID);
-    if (!ok) {
-        LOGERR("ContentDirectory::actSearch: no ContainerID in params\n");
-        return UPNP_E_INVALID_PARAM;
-    }
-    std::string in_SearchCriteria;
-    ok = sc.get("SearchCriteria", &in_SearchCriteria);
-    if (!ok) {
-        LOGERR("ContentDirectory::actSearch: no SearchCriteria in params\n");
-        return UPNP_E_INVALID_PARAM;
-    }
-    std::string in_Filter;
-    ok = sc.get("Filter", &in_Filter);
-    if (!ok) {
-        LOGERR("ContentDirectory::actSearch: no Filter in params\n");
-        return UPNP_E_INVALID_PARAM;
-    }
-    int in_StartingIndex;
-    ok = sc.get("StartingIndex", &in_StartingIndex);
-    if (!ok) {
-        LOGERR("ContentDirectory::actSearch: no StartingIndex in params\n");
-        return UPNP_E_INVALID_PARAM;
-    }
-    int in_RequestedCount;
-    ok = sc.get("RequestedCount", &in_RequestedCount);
-    if (!ok) {
-        LOGERR("ContentDirectory::actSearch: no RequestedCount in params\n");
-        return UPNP_E_INVALID_PARAM;
-    }
-    std::string in_SortCriteria;
-    ok = sc.get("SortCriteria", &in_SortCriteria);
-    if (!ok) {
-        LOGERR("ContentDirectory::actSearch: no SortCriteria in params\n");
-        return UPNP_E_INVALID_PARAM;
-    }
-
-    LOGDEB("ContentDirectory::actSearch: " << " ContainerID " << in_ContainerID << " SearchCriteria " << in_SearchCriteria << " Filter " << in_Filter << " StartingIndex " << in_StartingIndex << " RequestedCount " << in_RequestedCount << " SortCriteria " << in_SortCriteria << endl);
-
-    std::string out_Result;
-    std::string out_NumberReturned;
-    std::string out_TotalMatches;
-    std::string out_UpdateID;
-
-    out_NumberReturned = "0";
-    out_TotalMatches = "0";
-
-    data.addarg("Result", out_Result);
-    data.addarg("NumberReturned", out_NumberReturned);
-    data.addarg("TotalMatches", out_TotalMatches);
-    data.addarg("UpdateID", out_UpdateID);
-    return UPNP_E_SUCCESS;
-}
-
-