|
a/src/mediaserver/cdplugins/plgwithslave.cxx |
|
b/src/mediaserver/cdplugins/plgwithslave.cxx |
|
... |
|
... |
148 |
LOGERR("answer_to_connection: no media_uri for: " << url << endl);
|
148 |
LOGERR("answer_to_connection: no media_uri for: " << url << endl);
|
149 |
return MHD_NO;
|
149 |
return MHD_NO;
|
150 |
}
|
150 |
}
|
151 |
|
151 |
|
152 |
if (media_url.find("http") == 0) {
|
152 |
if (media_url.find("http") == 0) {
|
153 |
LOGDEB1("answer_to_connection: redirecting to " << media_url << endl);
|
|
|
154 |
|
|
|
155 |
static char data[] = "<html><body></body></html>";
|
153 |
static char data[] = "<html><body></body></html>";
|
156 |
struct MHD_Response *response =
|
154 |
struct MHD_Response *response =
|
157 |
MHD_create_response_from_buffer(strlen(data), data,
|
155 |
MHD_create_response_from_buffer(strlen(data), data,
|
158 |
MHD_RESPMEM_PERSISTENT);
|
156 |
MHD_RESPMEM_PERSISTENT);
|
159 |
if (response == NULL) {
|
157 |
if (response == NULL) {
|
|
... |
|
... |
285 |
}
|
283 |
}
|
286 |
|
284 |
|
287 |
static int resultToEntries(const string& encoded, int stidx, int cnt,
|
285 |
static int resultToEntries(const string& encoded, int stidx, int cnt,
|
288 |
vector<UpSong>& entries)
|
286 |
vector<UpSong>& entries)
|
289 |
{
|
287 |
{
|
290 |
entries.clear();
|
|
|
291 |
Json::Value decoded;
|
288 |
Json::Value decoded;
|
292 |
istringstream input(encoded);
|
289 |
istringstream input(encoded);
|
293 |
input >> decoded;
|
290 |
input >> decoded;
|
294 |
LOGDEB0("PlgWithSlave::results: got " << decoded.size() << " entries \n");
|
291 |
LOGDEB0("PlgWithSlave::results: got " << decoded.size() << " entries \n");
|
295 |
LOGDEB1("PlgWithSlave::results: undecoded: " << decoded.dump() << endl);
|
292 |
LOGDEB1("PlgWithSlave::results: undecoded: " << decoded.dump() << endl);
|
|
... |
|
... |
301 |
break;
|
298 |
break;
|
302 |
}
|
299 |
}
|
303 |
UpSong song;
|
300 |
UpSong song;
|
304 |
// tp is container ("ct") or item ("it")
|
301 |
// tp is container ("ct") or item ("it")
|
305 |
string stp = decoded[i].get("tp", "").asString();
|
302 |
string stp = decoded[i].get("tp", "").asString();
|
306 |
LOGDEB("PlgWithSlave::results: tp is " << stp << endl)
|
|
|
307 |
if (!stp.compare("ct")) {
|
303 |
if (!stp.compare("ct")) {
|
308 |
song.iscontainer = true;
|
304 |
song.iscontainer = true;
|
309 |
string ss = decoded[i].get("searchable", "").asString();
|
305 |
string ss = decoded[i].get("searchable", "").asString();
|
310 |
LOGDEB("PlgWithSlave::results: searchable is " << ss << endl)
|
|
|
311 |
if (!ss.empty()) {
|
306 |
if (!ss.empty()) {
|
312 |
song.searchable = stringToBool(ss);
|
307 |
song.searchable = stringToBool(ss);
|
313 |
}
|
308 |
}
|
314 |
} else if (!stp.compare("it")) {
|
309 |
} else if (!stp.compare("it")) {
|
315 |
song.iscontainer = false;
|
310 |
song.iscontainer = false;
|
316 |
JSONTOUPS(uri, uri);
|
311 |
JSONTOUPS(uri, uri);
|
317 |
JSONTOUPS(artist, dc:creator);
|
312 |
JSONTOUPS(artist, dc:creator);
|
318 |
JSONTOUPS(genre, upnp:genre);
|
313 |
JSONTOUPS(genre, upnp:genre);
|
319 |
JSONTOUPS(tracknum, upnp:originalTrackNumber);
|
314 |
JSONTOUPS(tracknum, upnp:originalTrackNumber);
|
|
|
315 |
JSONTOUPS(mime, res:mime);
|
|
|
316 |
string srate = decoded[i].get("res:samplefreq", "").asString();
|
|
|
317 |
if (!srate.empty()) {
|
|
|
318 |
song.samplefreq = atoi(srate.c_str());
|
|
|
319 |
}
|
320 |
string sdur = decoded[i].get("duration", "").asString();
|
320 |
string sdur = decoded[i].get("duration", "").asString();
|
321 |
if (!sdur.empty()) {
|
321 |
if (!sdur.empty()) {
|
322 |
song.duration_secs = atoi(sdur.c_str());
|
322 |
song.duration_secs = atoi(sdur.c_str());
|
323 |
}
|
323 |
}
|
324 |
} else {
|
324 |
} else {
|
|
... |
|
... |
328 |
JSONTOUPS(id, id);
|
328 |
JSONTOUPS(id, id);
|
329 |
JSONTOUPS(parentid, pid);
|
329 |
JSONTOUPS(parentid, pid);
|
330 |
JSONTOUPS(title, tt);
|
330 |
JSONTOUPS(title, tt);
|
331 |
JSONTOUPS(artUri, upnp:albumArtURI);
|
331 |
JSONTOUPS(artUri, upnp:albumArtURI);
|
332 |
JSONTOUPS(artist, upnp:artist);
|
332 |
JSONTOUPS(artist, upnp:artist);
|
|
|
333 |
JSONTOUPS(upnpClass, upnp:class);
|
|
|
334 |
LOGDEB1("PlgWitSlave::result: pushing: " << song.dump() << endl);
|
333 |
entries.push_back(song);
|
335 |
entries.push_back(song);
|
334 |
}
|
336 |
}
|
335 |
// We return the total match size, the count of actually returned
|
337 |
// We return the total match size, the count of actually returned
|
336 |
// entries can be obtained from the vector
|
338 |
// entries can be obtained from the vector
|
337 |
return decoded.size();
|
339 |
return decoded.size();
|
338 |
}
|
340 |
}
|
339 |
|
341 |
|
340 |
// Better return a bogus informative entry than an outright error:
|
342 |
// Better return a bogus informative entry than an outright error:
|
341 |
static int errorEntries(const string& pid, vector<UpSong>& entries)
|
343 |
static int errorEntries(const string& pid, vector<UpSong>& entries)
|
342 |
{
|
344 |
{
|
343 |
entries.clear();
|
|
|
344 |
entries.push_back(
|
345 |
entries.push_back(
|
345 |
UpSong::item(pid + "$bogus", pid,
|
346 |
UpSong::item(pid + "$bogus", pid,
|
346 |
"Service login or communication failure"));
|
347 |
"Service login or communication failure"));
|
347 |
return 1;
|
348 |
return 1;
|
348 |
}
|
349 |
}
|
|
... |
|
... |
350 |
int PlgWithSlave::browse(const string& objid, int stidx, int cnt,
|
351 |
int PlgWithSlave::browse(const string& objid, int stidx, int cnt,
|
351 |
vector<UpSong>& entries,
|
352 |
vector<UpSong>& entries,
|
352 |
const vector<string>& sortcrits,
|
353 |
const vector<string>& sortcrits,
|
353 |
BrowseFlag flg)
|
354 |
BrowseFlag flg)
|
354 |
{
|
355 |
{
|
355 |
LOGDEB("PlgWithSlave::browse\n");
|
356 |
LOGDEB1("PlgWithSlave::browse\n");
|
|
|
357 |
entries.clear();
|
356 |
if (!m->maybeStartCmd()) {
|
358 |
if (!m->maybeStartCmd()) {
|
357 |
return errorEntries(objid, entries);
|
359 |
return errorEntries(objid, entries);
|
358 |
}
|
360 |
}
|
359 |
string sbflg;
|
361 |
string sbflg;
|
360 |
switch (flg) {
|
362 |
switch (flg) {
|
|
... |
|
... |
380 |
}
|
382 |
}
|
381 |
return resultToEntries(it->second, stidx, cnt, entries);
|
383 |
return resultToEntries(it->second, stidx, cnt, entries);
|
382 |
}
|
384 |
}
|
383 |
|
385 |
|
384 |
|
386 |
|
|
|
387 |
class SearchCacheEntry {
|
|
|
388 |
public:
|
|
|
389 |
SearchCacheEntry()
|
|
|
390 |
: m_time(time(0)) {
|
|
|
391 |
}
|
|
|
392 |
time_t m_time;
|
|
|
393 |
vector<UpSong> m_results;
|
|
|
394 |
};
|
|
|
395 |
|
|
|
396 |
const int retention_secs = 300;
|
|
|
397 |
class SearchCache {
|
|
|
398 |
public:
|
|
|
399 |
SearchCache();
|
|
|
400 |
SearchCacheEntry *get(const string& query);
|
|
|
401 |
void set(const string& query, SearchCacheEntry &entry);
|
|
|
402 |
void flush();
|
|
|
403 |
private:
|
|
|
404 |
time_t m_lastflush;
|
|
|
405 |
unordered_map<string, SearchCacheEntry> m_cache;
|
|
|
406 |
};
|
|
|
407 |
|
|
|
408 |
SearchCache::SearchCache()
|
|
|
409 |
: m_lastflush(time(0))
|
|
|
410 |
{
|
|
|
411 |
}
|
|
|
412 |
|
|
|
413 |
void SearchCache::flush()
|
|
|
414 |
{
|
|
|
415 |
time_t now(time(0));
|
|
|
416 |
if (now - m_lastflush < 5) {
|
|
|
417 |
return;
|
|
|
418 |
}
|
|
|
419 |
for (unordered_map<string, SearchCacheEntry>::iterator it = m_cache.begin();
|
|
|
420 |
it != m_cache.end(); ) {
|
|
|
421 |
if (now - it->second.m_time > retention_secs) {
|
|
|
422 |
LOGDEB0("SearchCache::flush: erasing " << it->first << endl);
|
|
|
423 |
it = m_cache.erase(it);
|
|
|
424 |
} else {
|
|
|
425 |
it++;
|
|
|
426 |
}
|
|
|
427 |
}
|
|
|
428 |
m_lastflush = now;
|
|
|
429 |
}
|
|
|
430 |
|
|
|
431 |
SearchCacheEntry *SearchCache::get(const string& key)
|
|
|
432 |
{
|
|
|
433 |
flush();
|
|
|
434 |
auto it = m_cache.find(key);
|
|
|
435 |
if (it != m_cache.end()) {
|
|
|
436 |
LOGDEB0("SearchCache::get: found " << key << endl);
|
|
|
437 |
// we return a copy of the vector. Make our multi-access life simpler...
|
|
|
438 |
return new SearchCacheEntry(it->second);
|
|
|
439 |
}
|
|
|
440 |
LOGDEB0("SearchCache::get: not found " << key << endl);
|
|
|
441 |
return nullptr;
|
|
|
442 |
}
|
|
|
443 |
|
|
|
444 |
void SearchCache::set(const string& key, SearchCacheEntry &entry)
|
|
|
445 |
{
|
|
|
446 |
LOGDEB0("SearchCache::set: " << key << endl);
|
|
|
447 |
m_cache[key] = entry;
|
|
|
448 |
}
|
|
|
449 |
|
|
|
450 |
static SearchCache o_scache;
|
|
|
451 |
|
|
|
452 |
int resultFromCacheEntry(const string& classfilter, int stidx, int cnt,
|
|
|
453 |
const SearchCacheEntry& e,
|
|
|
454 |
vector<UpSong>& entries)
|
|
|
455 |
{
|
|
|
456 |
const vector<UpSong>& res = e.m_results;
|
|
|
457 |
LOGDEB0("resultFromCacheEntry: filter " << classfilter << " start " <<
|
|
|
458 |
stidx << " cnt " << cnt << " res.size " << res.size() << endl);
|
|
|
459 |
entries.reserve(cnt);
|
|
|
460 |
int total = 0;
|
|
|
461 |
for (unsigned int i = 0; i < res.size(); i++) {
|
|
|
462 |
if (!classfilter.empty() && res[i].upnpClass.find(classfilter) != 0) {
|
|
|
463 |
continue;
|
|
|
464 |
}
|
|
|
465 |
total++;
|
|
|
466 |
if (stidx > int(i)) {
|
|
|
467 |
continue;
|
|
|
468 |
}
|
|
|
469 |
if (int(entries.size()) >= cnt) {
|
|
|
470 |
continue;
|
|
|
471 |
}
|
|
|
472 |
LOGDEB1("resultFromCacheEntry: pushing class " << res[i].upnpClass <<
|
|
|
473 |
" tt " << res[i].title << endl);
|
|
|
474 |
entries.push_back(res[i]);
|
|
|
475 |
}
|
|
|
476 |
return total;
|
|
|
477 |
}
|
|
|
478 |
|
385 |
int PlgWithSlave::search(const string& ctid, int stidx, int cnt,
|
479 |
int PlgWithSlave::search(const string& ctid, int stidx, int cnt,
|
386 |
const string& searchstr,
|
480 |
const string& searchstr,
|
387 |
vector<UpSong>& entries,
|
481 |
vector<UpSong>& entries,
|
388 |
const vector<string>& sortcrits)
|
482 |
const vector<string>& sortcrits)
|
389 |
{
|
483 |
{
|
390 |
LOGDEB("PlgWithSlave::search\n");
|
484 |
LOGDEB1("PlgWithSlave::search\n");
|
|
|
485 |
entries.clear();
|
391 |
if (!m->maybeStartCmd()) {
|
486 |
if (!m->maybeStartCmd()) {
|
392 |
return errorEntries(ctid, entries);
|
487 |
return errorEntries(ctid, entries);
|
393 |
}
|
488 |
}
|
394 |
|
489 |
|
395 |
// Ok, so the upnp query language is quite powerful, but us, not
|
490 |
// Ok, so the upnp query language is quite powerful, but us, not
|
|
... |
|
... |
413 |
"]\n");
|
508 |
"]\n");
|
414 |
return errorEntries(ctid, entries);
|
509 |
return errorEntries(ctid, entries);
|
415 |
}
|
510 |
}
|
416 |
string slavefield;
|
511 |
string slavefield;
|
417 |
string value;
|
512 |
string value;
|
|
|
513 |
string classfilter;
|
418 |
for (unsigned int i = 0; i < vs.size()-2; i += 4) {
|
514 |
for (unsigned int i = 0; i < vs.size()-2; i += 4) {
|
419 |
const string& upnpproperty = vs[i];
|
515 |
const string& upnpproperty = vs[i];
|
420 |
LOGDEB("Looking at " << vs[i] << " " << vs[i+1] << " " <<
|
516 |
LOGDEB("PlgWithSlave::search:clause: " << vs[i] << " " << vs[i+1] <<
|
421 |
vs[i+2] << endl);
|
517 |
" " << vs[i+2] << endl);
|
422 |
if (!upnpproperty.compare("upnp:artist") ||
|
518 |
if (!upnpproperty.compare("upnp:artist") ||
|
423 |
!upnpproperty.compare("dc:author")) {
|
519 |
!upnpproperty.compare("dc:author")) {
|
424 |
slavefield = "artist";
|
520 |
slavefield = "artist";
|
425 |
value = vs[i+2];
|
521 |
value = vs[i+2];
|
426 |
break;
|
522 |
break;
|
|
... |
|
... |
430 |
break;
|
526 |
break;
|
431 |
} else if (!upnpproperty.compare("dc:title")) {
|
527 |
} else if (!upnpproperty.compare("dc:title")) {
|
432 |
slavefield = "track";
|
528 |
slavefield = "track";
|
433 |
value = vs[i+2];
|
529 |
value = vs[i+2];
|
434 |
break;
|
530 |
break;
|
|
|
531 |
} else if (!upnpproperty.compare("upnp:class")) {
|
|
|
532 |
classfilter = vs[i+2];
|
435 |
}
|
533 |
}
|
436 |
}
|
534 |
}
|
437 |
if (slavefield.empty()) {
|
535 |
if (slavefield.empty()) {
|
438 |
LOGERR("PlgWithSlave: unsupported search: [" << searchstr << "]\n");
|
536 |
LOGERR("PlgWithSlave: unsupported search: [" << searchstr << "]\n");
|
439 |
return errorEntries(ctid, entries);
|
537 |
return errorEntries(ctid, entries);
|
440 |
}
|
538 |
}
|
441 |
|
539 |
|
|
|
540 |
// In cache ?
|
|
|
541 |
SearchCacheEntry *cep;
|
|
|
542 |
string cachekey(m_name + ":" + value);
|
|
|
543 |
if ((cep = o_scache.get(cachekey)) != nullptr) {
|
|
|
544 |
int total = resultFromCacheEntry(classfilter, stidx,cnt, *cep, entries);
|
|
|
545 |
delete cep;
|
|
|
546 |
return total;
|
|
|
547 |
}
|
|
|
548 |
|
|
|
549 |
// Run query
|
442 |
unordered_map<string, string> res;
|
550 |
unordered_map<string, string> res;
|
443 |
if (!m->cmd.callproc("search", {
|
551 |
if (!m->cmd.callproc("search", {
|
444 |
{"objid", ctid},
|
552 |
{"objid", ctid},
|
445 |
{"field", slavefield},
|
|
|
446 |
{"value", value} }, res)) {
|
553 |
{"value", value} }, res)) {
|
447 |
LOGERR("PlgWithSlave::search: slave failure\n");
|
554 |
LOGERR("PlgWithSlave::search: slave failure\n");
|
448 |
return errorEntries(ctid, entries);
|
555 |
return errorEntries(ctid, entries);
|
449 |
}
|
556 |
}
|
450 |
|
557 |
|
451 |
auto it = res.find("entries");
|
558 |
auto it = res.find("entries");
|
452 |
if (it == res.end()) {
|
559 |
if (it == res.end()) {
|
453 |
LOGERR("PlgWithSlave::search: no entries returned\n");
|
560 |
LOGERR("PlgWithSlave::search: no entries returned\n");
|
454 |
return errorEntries(ctid, entries);
|
561 |
return errorEntries(ctid, entries);
|
455 |
}
|
562 |
}
|
456 |
return resultToEntries(it->second, stidx, cnt, entries);
|
563 |
// Convert the whole set and store in cache
|
|
|
564 |
SearchCacheEntry e;
|
|
|
565 |
resultToEntries(it->second, 0, 0, e.m_results);
|
|
|
566 |
o_scache.set(cachekey, e);
|
|
|
567 |
return resultFromCacheEntry(classfilter, stidx, cnt, e, entries);
|
457 |
}
|
568 |
}
|