|
a/src/mediaserver/cdplugins/plgwithslave.cxx |
|
b/src/mediaserver/cdplugins/plgwithslave.cxx |
|
... |
|
... |
64 |
|
64 |
|
65 |
class PlgWithSlave::Internal {
|
65 |
class PlgWithSlave::Internal {
|
66 |
public:
|
66 |
public:
|
67 |
Internal(PlgWithSlave *_plg, const string& exe, const string& hst,
|
67 |
Internal(PlgWithSlave *_plg, const string& exe, const string& hst,
|
68 |
int prt, const string& pp)
|
68 |
int prt, const string& pp)
|
69 |
: plg(_plg), exepath(exe), upnphost(hst), upnpport(prt), pathprefix(pp),
|
69 |
: plg(_plg), exepath(exe), upnphost(hst), upnpport(prt), pathprefix(pp),
|
70 |
laststream(this) {
|
70 |
laststream(this) {
|
71 |
}
|
71 |
}
|
72 |
|
72 |
|
73 |
bool maybeStartCmd();
|
73 |
bool maybeStartCmd();
|
74 |
|
74 |
|
|
... |
|
... |
116 |
// The 'plgi' here is just whatever plugin started up the httpd task
|
116 |
// The 'plgi' here is just whatever plugin started up the httpd task
|
117 |
// We just use it to find the appropriate plugin for this path,
|
117 |
// We just use it to find the appropriate plugin for this path,
|
118 |
// and then dispatch the request.
|
118 |
// and then dispatch the request.
|
119 |
PlgWithSlave::Internal *plgi = (PlgWithSlave::Internal*)cls;
|
119 |
PlgWithSlave::Internal *plgi = (PlgWithSlave::Internal*)cls;
|
120 |
PlgWithSlave *realplg =
|
120 |
PlgWithSlave *realplg =
|
121 |
dynamic_cast<PlgWithSlave*>(plgi->plg->m_services->getpluginforpath(url));
|
121 |
dynamic_cast<PlgWithSlave*>(plgi->plg->m_services->getpluginforpath(url));
|
122 |
if (nullptr == realplg) {
|
122 |
if (nullptr == realplg) {
|
123 |
LOGERR("answer_to_connection: no plugin for path [" << url << endl);
|
123 |
LOGERR("answer_to_connection: no plugin for path [" << url << endl);
|
124 |
return MHD_NO;
|
124 |
return MHD_NO;
|
125 |
}
|
125 |
}
|
126 |
|
126 |
|
|
... |
|
... |
243 |
// to tidal.
|
243 |
// to tidal.
|
244 |
string PlgWithSlave::get_media_url(const string& path)
|
244 |
string PlgWithSlave::get_media_url(const string& path)
|
245 |
{
|
245 |
{
|
246 |
LOGDEB0("PlgWithSlave::get_media_url: " << path << endl);
|
246 |
LOGDEB0("PlgWithSlave::get_media_url: " << path << endl);
|
247 |
if (!m->maybeStartCmd()) {
|
247 |
if (!m->maybeStartCmd()) {
|
248 |
return string();
|
248 |
return string();
|
249 |
}
|
249 |
}
|
250 |
time_t now = time(0);
|
250 |
time_t now = time(0);
|
251 |
if (m->laststream.path.compare(path) ||
|
251 |
if (m->laststream.path.compare(path) ||
|
252 |
(now - m->laststream.opentime > 10)) {
|
252 |
(now - m->laststream.opentime > 10)) {
|
253 |
unordered_map<string, string> res;
|
253 |
unordered_map<string, string> res;
|
254 |
if (!m->cmd.callproc("trackuri", {{"path", path}}, res)) {
|
254 |
if (!m->cmd.callproc("trackuri", {{"path", path}}, res)) {
|
255 |
LOGERR("PlgWithSlave::get_media_url: slave failure\n");
|
255 |
LOGERR("PlgWithSlave::get_media_url: slave failure\n");
|
256 |
return string();
|
256 |
return string();
|
257 |
}
|
257 |
}
|
258 |
|
258 |
|
259 |
auto it = res.find("media_url");
|
259 |
auto it = res.find("media_url");
|
260 |
if (it == res.end()) {
|
260 |
if (it == res.end()) {
|
261 |
LOGERR("PlgWithSlave::get_media_url: no media url in result\n");
|
261 |
LOGERR("PlgWithSlave::get_media_url: no media url in result\n");
|
262 |
return string();
|
262 |
return string();
|
263 |
}
|
263 |
}
|
264 |
m->laststream.clear();
|
264 |
m->laststream.clear();
|
265 |
m->laststream.path = path;
|
265 |
m->laststream.path = path;
|
266 |
m->laststream.media_url = it->second;
|
266 |
m->laststream.media_url = it->second;
|
267 |
m->laststream.opentime = now;
|
267 |
m->laststream.opentime = now;
|
268 |
}
|
268 |
}
|
|
... |
|
... |
285 |
{
|
285 |
{
|
286 |
delete m;
|
286 |
delete m;
|
287 |
}
|
287 |
}
|
288 |
|
288 |
|
289 |
static int resultToEntries(const string& encoded, int stidx, int cnt,
|
289 |
static int resultToEntries(const string& encoded, int stidx, int cnt,
|
290 |
vector<UpSong>& entries)
|
290 |
vector<UpSong>& entries)
|
291 |
{
|
291 |
{
|
292 |
Json::Value decoded;
|
292 |
Json::Value decoded;
|
293 |
istringstream input(encoded);
|
293 |
istringstream input(encoded);
|
294 |
input >> decoded;
|
294 |
input >> decoded;
|
295 |
LOGDEB0("PlgWithSlave::results: got " << decoded.size() << " entries \n");
|
295 |
LOGDEB0("PlgWithSlave::results: got " << decoded.size() << " entries \n");
|
296 |
LOGDEB1("PlgWithSlave::results: undecoded: " << decoded.dump() << endl);
|
296 |
LOGDEB1("PlgWithSlave::results: undecoded: " << decoded.dump() << endl);
|
297 |
bool dolimit = cnt > 0;
|
297 |
bool dolimit = cnt > 0;
|
298 |
|
298 |
|
299 |
for (unsigned int i = stidx; i < decoded.size(); i++) {
|
299 |
for (unsigned int i = stidx; i < decoded.size(); i++) {
|
300 |
#define JSONTOUPS(fld, nm) {song.fld = decoded[i].get(#nm, "").asString();}
|
300 |
#define JSONTOUPS(fld, nm) {song.fld = decoded[i].get(#nm, "").asString();}
|
301 |
if (dolimit && --cnt < 0) {
|
301 |
if (dolimit && --cnt < 0) {
|
302 |
break;
|
302 |
break;
|
303 |
}
|
303 |
}
|
304 |
UpSong song;
|
304 |
UpSong song;
|
|
|
305 |
JSONTOUPS(id, id);
|
|
|
306 |
JSONTOUPS(parentid, pid);
|
|
|
307 |
JSONTOUPS(title, tt);
|
|
|
308 |
JSONTOUPS(artUri, upnp:albumArtURI);
|
|
|
309 |
JSONTOUPS(artist, upnp:artist);
|
|
|
310 |
JSONTOUPS(upnpClass, upnp:class);
|
305 |
// tp is container ("ct") or item ("it")
|
311 |
// tp is container ("ct") or item ("it")
|
306 |
string stp = decoded[i].get("tp", "").asString();
|
312 |
string stp = decoded[i].get("tp", "").asString();
|
307 |
if (!stp.compare("ct")) {
|
313 |
if (!stp.compare("ct")) {
|
308 |
song.iscontainer = true;
|
314 |
song.iscontainer = true;
|
309 |
string ss = decoded[i].get("searchable", "").asString();
|
315 |
string ss = decoded[i].get("searchable", "").asString();
|
310 |
if (!ss.empty()) {
|
316 |
if (!ss.empty()) {
|
311 |
song.searchable = stringToBool(ss);
|
317 |
song.searchable = stringToBool(ss);
|
312 |
}
|
318 |
}
|
313 |
} else if (!stp.compare("it")) {
|
319 |
} else if (!stp.compare("it")) {
|
314 |
song.iscontainer = false;
|
320 |
song.iscontainer = false;
|
315 |
JSONTOUPS(uri, uri);
|
321 |
JSONTOUPS(uri, uri);
|
316 |
JSONTOUPS(artist, dc:creator);
|
322 |
JSONTOUPS(artist, dc:creator);
|
317 |
JSONTOUPS(genre, upnp:genre);
|
323 |
JSONTOUPS(genre, upnp:genre);
|
318 |
JSONTOUPS(album, upnp:album);
|
324 |
JSONTOUPS(album, upnp:album);
|
319 |
JSONTOUPS(tracknum, upnp:originalTrackNumber);
|
325 |
JSONTOUPS(tracknum, upnp:originalTrackNumber);
|
320 |
JSONTOUPS(mime, res:mime);
|
326 |
JSONTOUPS(mime, res:mime);
|
321 |
string srate = decoded[i].get("res:samplefreq", "").asString();
|
327 |
string srate = decoded[i].get("res:samplefreq", "").asString();
|
322 |
if (!srate.empty()) {
|
328 |
if (!srate.empty()) {
|
323 |
song.samplefreq = atoi(srate.c_str());
|
329 |
song.samplefreq = atoi(srate.c_str());
|
324 |
}
|
330 |
}
|
325 |
string sdur = decoded[i].get("duration", "").asString();
|
331 |
string sdur = decoded[i].get("duration", "").asString();
|
326 |
if (!sdur.empty()) {
|
332 |
if (!sdur.empty()) {
|
327 |
song.duration_secs = atoi(sdur.c_str());
|
333 |
song.duration_secs = atoi(sdur.c_str());
|
328 |
}
|
334 |
}
|
329 |
} else {
|
335 |
} else {
|
330 |
LOGERR("PlgWithSlave::result: bad type in entry: " << stp << endl);
|
336 |
LOGERR("PlgWithSlave::result: bad type in entry: " << stp <<
|
|
|
337 |
"(title: " << song.title << ")\n");
|
331 |
continue;
|
338 |
continue;
|
332 |
}
|
339 |
}
|
333 |
JSONTOUPS(id, id);
|
|
|
334 |
JSONTOUPS(parentid, pid);
|
|
|
335 |
JSONTOUPS(title, tt);
|
|
|
336 |
JSONTOUPS(artUri, upnp:albumArtURI);
|
|
|
337 |
JSONTOUPS(artist, upnp:artist);
|
|
|
338 |
JSONTOUPS(upnpClass, upnp:class);
|
|
|
339 |
LOGDEB1("PlgWitSlave::result: pushing: " << song.dump() << endl);
|
340 |
LOGDEB1("PlgWitSlave::result: pushing: " << song.dump() << endl);
|
340 |
entries.push_back(song);
|
341 |
entries.push_back(song);
|
341 |
}
|
342 |
}
|
342 |
// We return the total match size, the count of actually returned
|
343 |
// We return the total match size, the count of actually returned
|
343 |
// entries can be obtained from the vector
|
344 |
// entries can be obtained from the vector
|
344 |
return decoded.size();
|
345 |
return decoded.size();
|
345 |
}
|
346 |
}
|
|
... |
|
... |
461 |
BrowseFlag flg)
|
462 |
BrowseFlag flg)
|
462 |
{
|
463 |
{
|
463 |
LOGDEB1("PlgWithSlave::browse\n");
|
464 |
LOGDEB1("PlgWithSlave::browse\n");
|
464 |
entries.clear();
|
465 |
entries.clear();
|
465 |
if (!m->maybeStartCmd()) {
|
466 |
if (!m->maybeStartCmd()) {
|
466 |
return errorEntries(objid, entries);
|
467 |
return errorEntries(objid, entries);
|
467 |
}
|
468 |
}
|
468 |
string sbflg;
|
469 |
string sbflg;
|
469 |
switch (flg) {
|
470 |
switch (flg) {
|
470 |
case CDPlugin::BFMeta:
|
471 |
case CDPlugin::BFMeta:
|
471 |
sbflg = "meta";
|
472 |
sbflg = "meta";
|
|
... |
|
... |
487 |
}
|
488 |
}
|
488 |
}
|
489 |
}
|
489 |
|
490 |
|
490 |
unordered_map<string, string> res;
|
491 |
unordered_map<string, string> res;
|
491 |
if (!m->cmd.callproc("browse", {{"objid", objid}, {"flag", sbflg}}, res)) {
|
492 |
if (!m->cmd.callproc("browse", {{"objid", objid}, {"flag", sbflg}}, res)) {
|
492 |
LOGERR("PlgWithSlave::browse: slave failure\n");
|
493 |
LOGERR("PlgWithSlave::browse: slave failure\n");
|
493 |
return errorEntries(objid, entries);
|
494 |
return errorEntries(objid, entries);
|
494 |
}
|
495 |
}
|
495 |
|
496 |
|
496 |
auto it = res.find("entries");
|
497 |
auto it = res.find("entries");
|
497 |
if (it == res.end()) {
|
498 |
if (it == res.end()) {
|
498 |
LOGERR("PlgWithSlave::browse: no entries returned\n");
|
499 |
LOGERR("PlgWithSlave::browse: no entries returned\n");
|
499 |
return errorEntries(objid, entries);
|
500 |
return errorEntries(objid, entries);
|
500 |
}
|
501 |
}
|
501 |
|
502 |
|
502 |
if (flg == CDPlugin::BFChildren) {
|
503 |
if (flg == CDPlugin::BFChildren) {
|
503 |
ContentCacheEntry e;
|
504 |
ContentCacheEntry e;
|
|
... |
|
... |
521 |
const vector<string>& sortcrits)
|
522 |
const vector<string>& sortcrits)
|
522 |
{
|
523 |
{
|
523 |
LOGDEB("PlgWithSlave::search: [" << searchstr << "]\n");
|
524 |
LOGDEB("PlgWithSlave::search: [" << searchstr << "]\n");
|
524 |
entries.clear();
|
525 |
entries.clear();
|
525 |
if (!m->maybeStartCmd()) {
|
526 |
if (!m->maybeStartCmd()) {
|
526 |
return errorEntries(ctid, entries);
|
527 |
return errorEntries(ctid, entries);
|
527 |
}
|
528 |
}
|
528 |
|
529 |
|
|
|
530 |
// Computing a pre-cooked query. For simple-minded plugins.
|
|
|
531 |
// Note that none of the qobuz/gmusic/tidal plugins actually use
|
|
|
532 |
// the slavefield part (defining in what field the term should
|
|
|
533 |
// match).
|
|
|
534 |
//
|
529 |
// Ok, so the upnp query language is quite powerful, but us, not
|
535 |
// Ok, so the upnp query language is quite powerful, but us, not
|
530 |
// so much. We get rid of parenthesis and then try to find the
|
536 |
// so much. We get rid of parenthesis and then try to find the
|
531 |
// first searchExp on a field we can handle, pretend the operator
|
537 |
// first searchExp on a field we can handle, pretend the operator
|
532 |
// is "contains" and just do it. I so don't want to implement a
|
538 |
// is "contains" and just do it. I so don't want to implement a
|
533 |
// parser for the query language when the services don't support
|
539 |
// parser for the query language when the services don't support
|
|
... |
|
... |
541 |
stringToStrings(ss, vs);
|
547 |
stringToStrings(ss, vs);
|
542 |
|
548 |
|
543 |
// The sequence can now be either [field, op, value], or
|
549 |
// The sequence can now be either [field, op, value], or
|
544 |
// [field, op, value, and/or, field, op, value,...]
|
550 |
// [field, op, value, and/or, field, op, value,...]
|
545 |
if ((vs.size() + 1) % 4 != 0) {
|
551 |
if ((vs.size() + 1) % 4 != 0) {
|
546 |
LOGERR("PlgWithSlave::search: bad search string: [" << searchstr <<
|
552 |
LOGERR("PlgWithSlave::search: bad search string: [" << searchstr <<
|
547 |
"]\n");
|
553 |
"]\n");
|
548 |
return errorEntries(ctid, entries);
|
554 |
return errorEntries(ctid, entries);
|
549 |
}
|
555 |
}
|
550 |
string slavefield;
|
556 |
string slavefield;
|
551 |
string value;
|
557 |
string value;
|
552 |
string classfilter;
|
558 |
string classfilter;
|
553 |
string objkind;
|
559 |
string objkind;
|
|
... |
|
... |
569 |
|| beginswith(what, "object.container.playlist")) {
|
575 |
|| beginswith(what, "object.container.playlist")) {
|
570 |
objkind = "playlist";
|
576 |
objkind = "playlist";
|
571 |
}
|
577 |
}
|
572 |
classfilter = what;
|
578 |
classfilter = what;
|
573 |
} else if (!upnpproperty.compare("upnp:artist") ||
|
579 |
} else if (!upnpproperty.compare("upnp:artist") ||
|
574 |
!upnpproperty.compare("dc:author")) {
|
580 |
!upnpproperty.compare("dc:author")) {
|
575 |
slavefield = "artist";
|
581 |
slavefield = "artist";
|
576 |
value = vs[i+2];
|
582 |
value = vs[i+2];
|
577 |
break;
|
583 |
break;
|
578 |
} else if (!upnpproperty.compare("upnp:album")) {
|
584 |
} else if (!upnpproperty.compare("upnp:album")) {
|
579 |
slavefield = "album";
|
585 |
slavefield = "album";
|
|
... |
|
... |
583 |
slavefield = "track";
|
589 |
slavefield = "track";
|
584 |
value = vs[i+2];
|
590 |
value = vs[i+2];
|
585 |
break;
|
591 |
break;
|
586 |
}
|
592 |
}
|
587 |
}
|
593 |
}
|
588 |
if (slavefield.empty()) {
|
|
|
589 |
LOGERR("PlgWithSlave: unsupported search: [" << searchstr << "]\n");
|
|
|
590 |
return errorEntries(ctid, entries);
|
|
|
591 |
}
|
|
|
592 |
|
594 |
|
593 |
// In cache ?
|
595 |
// In cache ?
|
594 |
ContentCacheEntry *cep;
|
596 |
ContentCacheEntry *cep;
|
595 |
string cachekey(m_name + ":" + objkind + ":" + slavefield + ":" + value);
|
597 |
string cachekey(m_name + ":" + ctid + ":" + searchstr);
|
596 |
if ((cep = o_scache.get(cachekey)) != nullptr) {
|
598 |
if ((cep = o_scache.get(cachekey)) != nullptr) {
|
597 |
int total = cep->toResult(classfilter, stidx, cnt, entries);
|
599 |
int total = cep->toResult(classfilter, stidx, cnt, entries);
|
598 |
delete cep;
|
600 |
delete cep;
|
599 |
return total;
|
601 |
return total;
|
600 |
}
|
602 |
}
|
601 |
|
603 |
|
602 |
// Run query
|
604 |
// Run query
|
603 |
unordered_map<string, string> res;
|
605 |
unordered_map<string, string> res;
|
604 |
if (!m->cmd.callproc("search", {
|
606 |
if (!m->cmd.callproc("search", {
|
605 |
{"objid", ctid},
|
607 |
{"objid", ctid},
|
606 |
{"objkind", objkind},
|
608 |
{"objkind", objkind},
|
607 |
{"origsearch", searchstr},
|
609 |
{"origsearch", searchstr},
|
608 |
{"field", slavefield},
|
610 |
{"field", slavefield},
|
609 |
{"value", value} }, res)) {
|
611 |
{"value", value} }, res)) {
|
610 |
LOGERR("PlgWithSlave::search: slave failure\n");
|
612 |
LOGERR("PlgWithSlave::search: slave failure\n");
|
611 |
return errorEntries(ctid, entries);
|
613 |
return errorEntries(ctid, entries);
|
612 |
}
|
614 |
}
|
613 |
|
615 |
|
614 |
auto it = res.find("entries");
|
616 |
auto it = res.find("entries");
|
615 |
if (it == res.end()) {
|
617 |
if (it == res.end()) {
|
616 |
LOGERR("PlgWithSlave::search: no entries returned\n");
|
618 |
LOGERR("PlgWithSlave::search: no entries returned\n");
|
617 |
return errorEntries(ctid, entries);
|
619 |
return errorEntries(ctid, entries);
|
618 |
}
|
620 |
}
|
619 |
// Convert the whole set and store in cache
|
621 |
// Convert the whole set and store in cache
|
620 |
ContentCacheEntry e;
|
622 |
ContentCacheEntry e;
|
621 |
resultToEntries(it->second, 0, 0, e.m_results);
|
623 |
resultToEntries(it->second, 0, 0, e.m_results);
|
622 |
o_scache.set(cachekey, e);
|
624 |
o_scache.set(cachekey, e);
|