streaming plugins: arrange to share the c++ code between multiple services (hopefully)

Jean-Francois Dockes Jean-Francois Dockes 2016-08-30

changed src/cdplugin.hxx
changed src/contentdirectory.cxx
changed src/contentdirectory.hxx
changed Makefile.am
copied src/cdplugins/tidal/tidal.cxx -> src/cdplugins/plgwithslave.cxx
copied src/cdplugins/tidal/tidal.hxx -> src/cdplugins/plgwithslave.hxx
copied src/cdplugins/tidal/tidal.py -> src/cdplugins/tidal/tidal-app.py
src/cdplugin.hxx Diff Switch to unified view
Loading...
src/contentdirectory.cxx Diff Switch to unified view
Loading...
src/contentdirectory.hxx Diff Switch to unified view
Loading...
Makefile.am Diff Switch to unified view
Loading...
src/cdplugins/tidal/tidal.cxx to src/cdplugins/plgwithslave.cxx
--- a/src/cdplugins/tidal/tidal.cxx
+++ b/src/cdplugins/plgwithslave.cxx
@@ -15,7 +15,7 @@
  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  */
 
-#include "tidal.hxx"
+#include "plgwithslave.hxx"
 
 #define LOGGER_LOCAL_LOGINC 3
 
@@ -48,7 +48,7 @@
 
 class StreamHandle {
 public:
-    StreamHandle(Tidal::Internal *plg) {
+    StreamHandle(PlgWithSlave::Internal *plg) {
     }
     ~StreamHandle() {
         clear();
@@ -61,7 +61,7 @@
         opentime = 0;
     }
     
-    Tidal::Internal *plg;
+    PlgWithSlave::Internal *plg;
     AVIOContext *avio;
     string path;
     string media_url;
@@ -69,40 +69,32 @@
     time_t opentime;
 };
 
-class Tidal::Internal {
+class PlgWithSlave::Internal {
 public:
-    Internal(Tidal *tidal, const vector<string>& pth, const string& hst,
+    Internal(PlgWithSlave *_plg, const string& exe, const string& hst,
              int prt, const string& pp)
-	: plg(tidal), path(pth), host(hst), port(prt), pathprefix(pp), kbs(0),
-          laststream(this),
-          mhd(0) { }
-
-    bool maybeStartCmd(const string&);
-    string get_media_url(const std::string& path);
-    // This also sets the kbs
-    string get_mimetype(const std::string& path);
-
-    Tidal *plg;
+	: plg(_plg), exepath(exe), host(hst), port(prt), pathprefix(pp), 
+          laststream(this) {
+    }
+
+    bool maybeStartCmd();
+
+    PlgWithSlave *plg;
     CmdTalk cmd;
-    vector<string> path;
+    string exepath;
     // Host and port for the URLs we generate.
     string host;
     int port;
-    // path prefix (this is used by upmpdcli that a gets is for us).
+    // path prefix (this is used by upmpdcli that gets it for us).
     string pathprefix;
-    // mimetype is a constant for a given session, depend on quality
-    // choice only. Initialized once
-    string mimetype;
-    // kilobits/s
-    int kbs;
     
-    // Cached uri translation and stream: set in getinfo() and reused
-    // in open() (used with miniserver/vdir)
+    // Cached uri translation
     StreamHandle laststream;
-    // When using microhttpd
-    struct MHD_Daemon *mhd;
 };
 
+// microhttpd daemon handle. There is only one of these, and one port, we find
+// the right plugin by looking at the url path.
+static struct MHD_Daemon *mhd;
 
 // Microhttpd connection handler. We re-build the complete url + query
 // string (&trackid=value), use this to retrieve a Tidal URL, and
@@ -116,16 +108,34 @@
                                 const char *upload_data, 
                                 size_t *upload_data_size, void **con_cls)
 {
-    LOGDEB("answer_to_connection: url " << url << " method " << method << 
-           " version " << version << endl);
-    Tidal::Internal *me = (Tidal::Internal*)cls;
-    
     static int aptr;
     if (&aptr != *con_cls) {
         /* do not respond on first call */
         *con_cls = &aptr;
         return MHD_YES;
     }
+
+    LOGDEB("answer_to_connection: url " << url << " method " << method << 
+           " version " << version << endl);
+
+    // The 'plgi' here is just whatever plugin started up the httpd task
+    // We just use it to find the appropriate plugin for this path,
+    // and then dispatch the request.
+    PlgWithSlave::Internal *plgi = (PlgWithSlave::Internal*)cls;
+    PlgWithSlave *realplg =
+      dynamic_cast<PlgWithSlave*>(plgi->plg->m_services->getpluginforpath(url));
+    if (nullptr == realplg) {
+        LOGERR("answer_to_connection: no plugin for path [" << url << endl);
+        return MHD_NO;
+    }
+
+    // We may need one day to subclass PlgWithSlave to implement a
+    // plugin-specific method. For now, existing plugins have
+    // compatible python code, and we can keep one c++ method.
+    // get_media_url() would also need changing because it accesses Internal:
+    // either make it generic or move to subclass.
+    //return realplg->answer_to_connection(connection, url, method, version,
+    //                               upload_data, upload_data_size, con_cls);
 
     // Rebuild URL + query
     string path(url);
@@ -138,15 +148,15 @@
     }
     path += string("?version=1&trackId=") + stid;
 
-    // Translate to Tidal URL
-    string media_url = me->get_media_url(path);
+    // Translate to Tidal/Qobuz etc real temporary URL
+    string media_url = realplg->get_media_url(path);
     if (media_url.empty()) {
         LOGERR("answer_to_connection: no media_uri for: " << url << endl);
         return MHD_NO;
     }
 
     if (media_url.find("http") == 0) {
-        LOGDEB("Tidal: redirecting to " << media_url << endl);
+        LOGDEB("answer_to_connection: redirecting to " << media_url << endl);
 
         static char data[] = "<html><body></body></html>";
         struct MHD_Response *response =
@@ -161,8 +171,8 @@
         MHD_destroy_response(response);
         return ret;
     } else {
-        LOGERR("Tidal: got non-http URL !: " << media_url << endl);
-        LOGERR("Tidal:   the code for handling these is gone !\n");
+        LOGERR("PlgWithSlave: got non-http URL !: " << media_url << endl);
+        LOGERR("PlgWithSlave:   the code for handling these is gone !\n");
         LOGERR("    will have to fetch it from git history\n");
         return MHD_NO;
     } 
@@ -174,123 +184,99 @@
 }
 
 // Called once for starting the Python program and do other initialization.
-bool Tidal::Internal::maybeStartCmd(const string& who)
-{
-    LOGDEB("Tidal::maybeStartCmd for: " << who << endl);
-    
+bool PlgWithSlave::Internal::maybeStartCmd()
+{
     if (cmd.running()) {
         return true;
     }
 
-    ConfSimple *conf = plg->m_services->getconfig(plg);
-    string tidalquality;
-    if (!conf || !conf->get("tidalquality", tidalquality)) {
-        LOGERR("Tidal: can't get parameter 'tidalquality' from config\n");
-        return false;
+    if (nullptr == mhd) {
+
+        // Start the microhttpd daemon. There can be only one, and it
+        // is started with a context handle which points to whatever
+        // plugin got there first. The callback will only use the
+        // handle to get to the plugin services, and retrieve the
+        // appropriate plugin based on the url path prefix.
+        LOGDEB("PlgWithSlave: starting httpd on port "<< port << endl);
+        ConfSimple *conf = plg->m_services->getconfig(plg);
+        port = 49149;
+        string sport;
+        if (conf->get("plgmicrohttpport", sport)) {
+            port = atoi(sport.c_str());
+        }
+        mhd = MHD_start_daemon(
+            MHD_USE_THREAD_PER_CONNECTION,
+            //MHD_USE_SELECT_INTERNALLY, 
+            port, 
+            /* Accept policy callback and arg */
+            accept_policy, NULL, 
+            /* handler and arg */
+            &answer_to_connection, this, 
+            MHD_OPTION_END);
+        if (nullptr == mhd) {
+            LOGERR("PlgWithSlave: MHD_start_daemon failed\n");
+            return false;
+        }
     }
     
-    port = 49149;
-    string sport;
-    if (conf->get("tidalmicrohttpport", sport)) {
-        port = atoi(sport.c_str());
-    }
-    mhd = MHD_start_daemon(
-        MHD_USE_THREAD_PER_CONNECTION,
-        //MHD_USE_SELECT_INTERNALLY, 
-        port, 
-        /* Accept policy callback and arg */
-        accept_policy, NULL, 
-        /* handler and arg */
-        &answer_to_connection, this, 
-        MHD_OPTION_END);
-    if (nullptr == mhd) {
-        LOGERR("Tidal: MHD_start_daemon failed\n");
-        return false;
-    }
-        
     string pythonpath = string("PYTHONPATH=") +
-        path_cat(g_datadir, "cdplugins/pycommon");
+        path_cat(g_datadir, "cdplugins") + ":" +
+        path_cat(g_datadir, "cdplugins/pycommon") + ":" +
+        path_cat(g_datadir, "cdplugins/" + plg->m_name);
     string configname = string("UPMPD_CONFIG=") + g_configfilename;
     stringstream ss;
     ss << host << ":" << port;
     string hostport = string("UPMPD_HTTPHOSTPORT=") + ss.str();
     string pp = string("UPMPD_PATHPREFIX=") + pathprefix;
-    if (!cmd.startCmd("tidal.py", {/*args*/},
-                      /* env */ {pythonpath, configname, hostport, pp},
-                      /* exec path */ path)) {
-        LOGERR("Tidal::maybeStartCmd: " << who << " startCmd failed\n");
+    if (!cmd.startCmd(exepath, {/*args*/},
+                      /* env */ {pythonpath, configname, hostport, pp})) {
+        LOGERR("PlgWithSlave::maybeStartCmd: startCmd failed\n");
         return false;
     }
     return true;
 }
 
-string Tidal::Internal::get_mimetype(const std::string& path)
-{
-    if (!maybeStartCmd("get_mimetype")) {
-	return string();
-    }
-    if (mimetype.empty()) {
-	unordered_map<string, string> res;
-	if (!cmd.callproc("mimetype", {{"path", path}}, res)) {
-	    LOGERR("Tidal::get_mimetype: slave failure\n");
-	    return string();
-	}
-
-	auto it = res.find("mimetype");
-	if (it == res.end()) {
-	    LOGERR("Tidal::get_mimetype: no mimetype in result\n");
-	    return string();
-	}
-	mimetype = it->second;
-	it = res.find("kbs");
-	if (it != res.end()) {
-            kbs = atoi(it->second.c_str());
-	}
-	LOGDEB("Tidal: got mimetype [" << mimetype << "]\n");
-    }
-    return mimetype;
-}
-
-// Translate our http URL (based on the trackid), to an actual
-// temporary TIDAL one, which will be an HTTP URL pointing to either
-// an AAC or a FLAC stream.
+// Translate the slave-generated HTTP URL (based on the trackid), to
+// an actual temporary service (e.g. tidal one), which will be an HTTP
+// URL pointing to either an AAC or a FLAC stream.
 // Older versions of this module handled AAC FLV transported over
 // RTMP, apparently because of the use of a different API key. Look up
 // the git history if you need this again.
-// The Python code calls tidal.com to translate the trackid to a temp
+// The Python code calls the service to translate the trackid to a temp
 // URL. We cache the result for a few seconds to avoid multiple calls
 // to tidal.
-string Tidal::Internal::get_media_url(const std::string& path)
-{
-    LOGDEB("Tidal::get_media_url: " << path << endl);
-    if (!maybeStartCmd("get_media_url")) {
+string PlgWithSlave::get_media_url(const std::string& path)
+{
+    LOGDEB("PlgWithSlave::get_media_url: " << path << endl);
+    if (!m->maybeStartCmd()) {
 	return string();
     }
     time_t now = time(0);
-    if (laststream.path.compare(path) || (now - laststream.opentime > 10)) {
+    if (m->laststream.path.compare(path) ||
+        (now - m->laststream.opentime > 10)) {
 	unordered_map<string, string> res;
-	if (!cmd.callproc("trackuri", {{"path", path}}, res)) {
-	    LOGERR("Tidal::get_media_url: slave failure\n");
+	if (!m->cmd.callproc("trackuri", {{"path", path}}, res)) {
+	    LOGERR("PlgWithSlave::get_media_url: slave failure\n");
 	    return string();
 	}
 
 	auto it = res.find("media_url");
 	if (it == res.end()) {
-	    LOGERR("Tidal::get_media_url: no media url in result\n");
+	    LOGERR("PlgWithSlave::get_media_url: no media url in result\n");
 	    return string();
 	}
-        laststream.clear();
-        laststream.path = path;
-        laststream.media_url = it->second;
-        laststream.opentime = now;
-    }
-
-    LOGDEB("Tidal: got media url [" << laststream.media_url << "]\n");
-    return laststream.media_url;
-}
-
-
-Tidal::Tidal(const std::string& name, CDPluginServices *services)
+        m->laststream.clear();
+        m->laststream.path = path;
+        m->laststream.media_url = it->second;
+        m->laststream.opentime = now;
+    }
+
+    LOGDEB("PlgWithSlave: got media url [" << m->laststream.media_url << "]\n");
+    return m->laststream.media_url;
+}
+
+
+PlgWithSlave::PlgWithSlave(const std::string& name, CDPluginServices *services)
     : CDPlugin(name, services)
 {
     m = new Internal(this, services->getexecpath(this),
@@ -299,7 +285,7 @@
                      services->getpathprefix(this));
 }
 
-Tidal::~Tidal()
+PlgWithSlave::~PlgWithSlave()
 {
     delete m;
 }
@@ -308,8 +294,8 @@
 			   std::vector<UpSong>& entries)
 {
     auto decoded = json::parse(encoded);
-    LOGDEB("Tidal::results: got " << decoded.size() << " entries\n");
-    LOGDEB1("Tidal::results: undecoded json: " << decoded.dump() << endl);
+    LOGDEB("PlgWithSlave::results: got " << decoded.size() << " entries\n");
+    LOGDEB1("PlgWithSlave::results: undecoded json: " << decoded.dump() << endl);
 
     for (unsigned int i = stidx; i < decoded.size(); i++) {
 	if (--cnt < 0) {
@@ -319,7 +305,7 @@
 	// tp is container ("ct") or item ("it")
 	auto it1 = decoded[i].find("tp");
 	if (it1 == decoded[i].end()) {
-	    LOGERR("Tidal::browse: no type in entry\n");
+	    LOGERR("PlgWithSlave::browse: no type in entry\n");
 	    continue;
 	}
 	string stp = it1.value();
@@ -343,7 +329,7 @@
 	    JSONTOUPS(artUri, upnp:albumArtURI);
 	    JSONTOUPS(duration_secs, duration);
 	} else {
-	    LOGERR("Tidal::browse: bad type in entry: " << it1.value() << endl);
+	    LOGERR("PlgWithSlave::browse: bad type in entry: " << it1.value() << endl);
 	    continue;
 	}
 	JSONTOUPS(id, id);
@@ -356,13 +342,13 @@
     return decoded.size();
 }
 
-int Tidal::browse(const std::string& objid, int stidx, int cnt,
+int PlgWithSlave::browse(const std::string& objid, int stidx, int cnt,
 		  std::vector<UpSong>& entries,
 		  const std::vector<std::string>& sortcrits,
 		  BrowseFlag flg)
 {
-    LOGDEB("Tidal::browse\n");
-    if (!m->maybeStartCmd("browse")) {
+    LOGDEB("PlgWithSlave::browse\n");
+    if (!m->maybeStartCmd()) {
 	return -1;
     }
     string sbflg;
@@ -378,63 +364,63 @@
 
     unordered_map<string, string> res;
     if (!m->cmd.callproc("browse", {{"objid", objid}, {"flag", sbflg}}, res)) {
-	LOGERR("Tidal::browse: slave failure\n");
+	LOGERR("PlgWithSlave::browse: slave failure\n");
 	return -1;
     }
 
     auto it = res.find("entries");
     if (it == res.end()) {
-	LOGERR("Tidal::browse: no entries returned\n");
+	LOGERR("PlgWithSlave::browse: no entries returned\n");
 	return -1;
     }
     return resultToEntries(it->second, stidx, cnt, entries);
 }
 
 
-int Tidal::search(const string& ctid, int stidx, int cnt,
+int PlgWithSlave::search(const string& ctid, int stidx, int cnt,
 		  const string& searchstr,
 		  vector<UpSong>& entries,
 		  const vector<string>& sortcrits)
 {
-    LOGDEB("Tidal::search\n");
-    if (!m->maybeStartCmd("search")) {
+    LOGDEB("PlgWithSlave::search\n");
+    if (!m->maybeStartCmd()) {
 	return -1;
     }
 
     // We only accept field xx value as search criteria
     vector<string> vs;
     stringToStrings(searchstr, vs);
-    LOGDEB("Tidal::search:search string split->" << vs.size() << " pieces\n");
     if (vs.size() != 3) {
-	LOGERR("Tidal::search: bad search string: [" << searchstr << "]\n");
+	LOGERR("PlgWithSlave::search: bad search string: [" << searchstr <<
+               "]\n");
 	return -1;
     }
     const string& upnpproperty = vs[0];
-    string tidalfield;
+    string slavefield;
     if (!upnpproperty.compare("upnp:artist") ||
 	!upnpproperty.compare("dc:author")) {
-	tidalfield = "artist";
+	slavefield = "artist";
     } else if (!upnpproperty.compare("upnp:album")) {
-	tidalfield = "album";
+	slavefield = "album";
     } else if (!upnpproperty.compare("dc:title")) {
-	tidalfield = "track";
+	slavefield = "track";
     } else {
-	LOGERR("Tidal::search: bad property: [" << upnpproperty << "]\n");
+	LOGERR("PlgWithSlave::search: bad property: [" << upnpproperty << "]\n");
 	return -1;
     }
 	
     unordered_map<string, string> res;
     if (!m->cmd.callproc("search", {
 		{"objid", ctid},
-		{"field", tidalfield},
+		{"field", slavefield},
 		{"value", vs[2]} },  res)) {
-	LOGERR("Tidal::search: slave failure\n");
+	LOGERR("PlgWithSlave::search: slave failure\n");
 	return -1;
     }
 
     auto it = res.find("entries");
     if (it == res.end()) {
-	LOGERR("Tidal::search: no entries returned\n");
+	LOGERR("PlgWithSlave::search: no entries returned\n");
 	return -1;
     }
     return resultToEntries(it->second, stidx, cnt, entries);
src/cdplugins/tidal/tidal.hxx to src/cdplugins/plgwithslave.hxx
--- a/src/cdplugins/tidal/tidal.hxx
+++ b/src/cdplugins/plgwithslave.hxx
@@ -14,8 +14,8 @@
  *   Free Software Foundation, Inc.,
  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  */
-#ifndef _TIDAL_H_INCLUDED_
-#define _TIDAL_H_INCLUDED_
+#ifndef _PLGWITHSLAVE_H_INCLUDED_
+#define _PLGWITHSLAVE_H_INCLUDED_
 
 #include <vector>
 
@@ -23,10 +23,10 @@
 #include "libupnpp/device/vdir.hxx"
 
 // Tidal interface
-class Tidal : public CDPlugin {
+class PlgWithSlave : public CDPlugin {
 public:
-    Tidal(const std::string& name, CDPluginServices *services);
-    virtual ~Tidal();
+    PlgWithSlave(const std::string& name, CDPluginServices *services);
+    virtual ~PlgWithSlave();
 
     // Returns totalmatches
     virtual int browse(
@@ -41,9 +41,11 @@
 	std::vector<UpSong>& entries,
 	const std::vector<std::string>& sortcrits = std::vector<std::string>());
 
+    virtual std::string get_media_url(const std::string& path);
+
     class Internal;
 private:
     Internal *m;
 };
 
-#endif /* _TIDAL_H_INCLUDED_ */
+#endif /* _PLGWITHSLAVE_H_INCLUDED_ */
src/cdplugins/tidal/tidal.py to src/cdplugins/tidal/tidal-app.py
--- a/src/cdplugins/tidal/tidal.py
+++ b/src/cdplugins/tidal/tidal-app.py
@@ -149,12 +149,6 @@
     else:
         return ('video/x-flv', str(96))
 
-@dispatcher.record('mimetype')
-def mimetype(a):
-    maybelogin()
-    mime, kbs = get_mimeandkbs()
-    return {'mimetype' : mime, 'kbs' : kbs}
-
 @dispatcher.record('trackuri')
 def trackuri(a):
     msgproc.log("trackuri: [%s]" % a)