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
}