--- a/src/ohcredentials.cxx
+++ b/src/ohcredentials.cxx
@@ -28,6 +28,7 @@
 #include <map>
 #include <utility>
 #include <vector>
+#include <regex>
 
 #include "conftree.h"
 #include "main.hxx"
@@ -39,6 +40,7 @@
 #include "libupnpp/base64.hxx"
 #include "libupnpp/soaphelp.hxx"
 #include "libupnpp/device/device.hxx"
+#include "mediaserver/cdplugins/cdplugin.hxx"
 
 using namespace std;
 using namespace std::placeholders;
@@ -52,27 +54,88 @@
     {"qobuz.com", "qobuz"}
 };
 
-// Should be derived into ServiceCredsQobuz, ServiceCredsTidal, there
-// is a lot in common and a few diffs.
+// This is used for translating urls for the special use of
+// Kazoo/Lumin. The media server, which is used to run the http server
+// and for getting the real media URLs, must run on this host (for one
+// thing the creds are passed through a local file).
+// *** Note that this needs xxxautostart to work, else the HTTP server
+//     won't be listening (as long as nobody accesses the app section
+//     of the media server) ***
+static string upnphost;
+
+// Called from OHPlaylist. The CP (Kazoo/Lumin mostly) will send URIs
+// like qobuz:// tidal:// and expect the renderer to know what to do
+// with them. We transform them so that they point to our media server
+// gateway (which should be running of course for this to work).
+bool OHCredsMaybeMorphSpecialUri(string& uri)
+{
+    if (uri.find("http://") == 0 || uri.find("https://") == 0) {
+        return true;
+    }
+
+    static string sport;
+    if (sport.empty()) {
+        std::unique_lock<std::mutex>(g_configlock);
+        int port = CDPluginServices::default_microhttpport();
+        if (!g_config->get("plgmicrohttpport", sport)) {
+            sport = SoapHelp::i2s(port);
+        }
+    }
+
+    // http://wiki.openhome.org/wiki/Av:Developer:Eriskay:StreamingServices
+    // Tidal and qobuz tracks added by Kazoo / Lumin: 
+    //   tidal://track?version=1&trackId=[tidal_track_id]
+    //   qobuz://track?version=2&trackId=[qobuz_track_id]
+    
+    string se =
+        "(tidal|qobuz)://track\\?version=([[:digit:]]+)&trackId=([[:digit:]]+)";
+    std::regex e(se);
+    std::smatch mr;
+    bool found = std::regex_match(uri, mr, e);
+    if (found) {
+        string pathprefix = CDPluginServices::getpathprefix(mr[1]);
+
+        // The microhttpd code actually only cares about getting a
+        // trackId parameter. Make it look what the plugins normally
+        // generate anyway:
+        string path = path_cat(pathprefix,
+                               "track?version=1&trackId=" + mr[3].str());
+        uri = string("http://") + upnphost + ":" + sport + path;
+    }
+    return found;
+}
+
+// We might want to derive this into ServiceCredsQobuz,
+// ServiceCredsTidal, there is a lot in common and a few diffs.
 struct ServiceCreds {
     ServiceCreds() {}
     ServiceCreds(const string& inm, const string& u, const string& p,
                  const string& ep)
         : servicename(inm), user(u), password(p), encryptedpass(ep) {
-        // The appid used by the Qobuz python module. Has to be
-        // consistent with the token obtained by the same, so we
-        // return it (by convention, as seen in wiresharking kazoo) in
-        // the data field. Of course, this class should be derived
-        // into service-specific ones, and this should happen only for
-        // the qobuz version. We could and do obtain the appid from
-        // the module, but kazoo wants it before we login, so just
-        // hard-code it for now.
-        data = "285473059";
-    }
+
+        if (servicename == "qobuz") {
+            // The appid used by the Qobuz python module. Has to be
+            // consistent with the token obtained by the same, so we
+            // return it (by convention, as seen in wiresharking
+            // kazoo) in the data field. We could and do obtain the
+            // appid from the module, but kazoo apparently wants it
+            // before we login, so just hard-code it for now.  The
+            // Python code uses the value from XBMC,285473059,
+            // ohplayer uses 854233864
+            data = "285473059";
+        } else if (servicename == "tidal") {
+            // data contains the country code
+            data = "FR";
+        }
+    }
+
     ~ServiceCreds() {
         delete cmd;
     }
 
+    // We need a Python helper to perform the login. That's the media
+    // server gateway module, from which we only use a separate method
+    // which logs-in and returns the auth data (token, etc.)
     bool maybeStartCmd() {
         LOGDEB("ServiceCreds: " << servicename << " maybeStartCmd()\n");
         if (nullptr == cmd) {
@@ -90,8 +153,13 @@
             path_cat(g_datadir, "cdplugins") + ":" +
             path_cat(g_datadir, "cdplugins/pycommon") + ":" +
             path_cat(g_datadir, "cdplugins/" + servicename);
+        string configname = string("UPMPD_CONFIG=") + g_configfilename;
+        // hostport is not needed by this login-only instance.
+        string hostport = string("UPMPD_HTTPHOSTPORT=bogus:0");
+        string pp = string("UPMPD_PATHPREFIX=") +
+            CDPluginServices::getpathprefix(servicename);
         if (!cmd->startCmd(exepath, {/*args*/},
-                          /* env */ {pythonpath})) {
+                           /* env */ {pythonpath, configname, hostport, pp})) {
             LOGERR("ServiceCreds::maybeStartCmd: startCmd failed\n");
             return false;
         }
@@ -101,9 +169,17 @@
 
     string login() {
         LOGDEB("ServiceCreds: " << servicename << " login\n");
-        if (!token.empty()) {
-            return token;
-        }
+
+        // Check if already logged-in
+        if (servicename == "qobuz" || servicename == "tidal") {
+            if (!servicedata["token"].empty()) {
+                return servicedata["token"];
+            }
+        } else {
+            LOGERR("Unsupported service: " << servicename << endl);
+            return string();
+        }
+
         if (!maybeStartCmd()) {
             return string();
         }
@@ -114,32 +190,42 @@
                    servicename << " user " << user << endl);
             return string();
         }
-        auto it = res.find("appid");
-        if (it == res.end()) {
-            LOGERR("ServiceCreds::login: no appid. Service " <<
-                   servicename << " user " << user << endl);
-            return string();
-        }
-        appid = it->second;
-        data = appid;
-        it = res.find("token");
-        if (it == res.end()) {
-            LOGERR("ServiceCreds::login: no token. Service " <<
-                   servicename << " user " << user << endl);
-            return string();
-        }
-        return token = it->second;
+
+        vector<string> toknames;
+        if (servicename == "qobuz") {
+            toknames = vector<string>{"token", "appid"};
+        } else if (servicename == "tidal") {
+            toknames = vector<string>{"token", "country"};
+        }
+        for (const auto& toknm : toknames) {
+            auto it = res.find(toknm);
+            if (it == res.end()) {
+                LOGERR("ServiceCreds::login: no " << toknm << ". Service " <<
+                       servicename << " user " << user << endl);
+                return string();
+            }
+            servicedata[toknm] = it->second;
+        }
+        if (servicename == "qobuz") {
+            data = servicedata["appid"];
+        } else if (servicename == "tidal") {
+            data = servicedata["country"];
+        }
+        return servicedata["token"];
     }
 
     void logout() {
-        appid.clear();
-        token.clear();
+        servicedata.clear();
     }
 
     string str() {
         string s;
+        string sdata;
+        for (const auto& entry:servicedata) {
+            sdata += entry.first + " : " + entry.second + ", ";
+        }
         s += "Service: " + servicename + " User: " + user +
-            /*" Pass: "+password*/ + " Appid " + appid + " Token " + token +
+            /*" Pass: "+password*/ + " Servicedata: " + sdata +
             /*" EPass: "+encryptedpass*/ + " Enabled: " +
             SoapHelp::i2s(enabled) + " Status: " + status + " Data: " + data;
         return s;
@@ -150,14 +236,14 @@
     string user;
     string password;
     string encryptedpass;
-    string appid;
-    string token;
     bool enabled{true};
+    CmdTalk *cmd{0};
+    // Things we obtain from the module and send to the CP
+    unordered_map<string,string> servicedata;
+
     string status;
-    // For qobuz, data contains the "app id" 854233864 for ohplayer.
-    // XBMC is 285473059
+    // See comments about 'data' use above.
     string data;
-    CmdTalk *cmd{0};
 };
 
 class OHCredentials::Internal {
@@ -214,18 +300,17 @@
 
     bool save() {
         string credsfile = path_cat(cachedir, "screds");
-        FILE *fp = fopen(credsfile.c_str(), "w");
-        fchmod(fileno(fp), 0600);
-        if (nullptr == fp) {
+        ConfSimple credsconf(credsfile.c_str());
+        if (!credsconf.ok()) {
             LOGERR("OHCredentials: error opening " << credsfile <<
                    " errno " << errno << endl);
             return false;
         }
         for (const auto& cred : creds) {
-            fprintf(fp, "[%s]\nu=%s\np=%s\n", cred.second.servicename.c_str(),
-                    cred.second.user.c_str(), cred.second.password.c_str());
-        }
-        fclose(fp);
+            credsconf.set("u", cred.second.user, cred.second.servicename);
+            credsconf.set("p", cred.second.password, cred.second.servicename);
+        }
+        chmod(credsfile.c_str(), 0600);
         return true;
     }
     
@@ -268,6 +353,9 @@
     dev->addActionMapping(
         this, "GetSequenceNumber",
         bind(&OHCredentials::actGetSequenceNumber, this, _1, _2));
+
+    unsigned short usport;
+    dev->ipv4(&upnphost, &usport);
 }
 
 OHCredentials::~OHCredentials()