Switch to unified view

a/src/ohradio.cxx b/src/ohradio.cxx
...
...
12
 *   You should have received a copy of the GNU General Public License
12
 *   You should have received a copy of the GNU General Public License
13
 *   along with this program; if not, write to the
13
 *   along with this program; if not, write to the
14
 *   Free Software Foundation, Inc.,
14
 *   Free Software Foundation, Inc.,
15
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
16
 */
16
 */
17
#define LOGGER_LOCAL_LOGINC 3
17
18
18
#include "ohradio.hxx"
19
#include "ohradio.hxx"
19
20
20
#include <stdlib.h>
21
#include <stdlib.h>
21
22
...
...
49
using namespace std::placeholders;
50
using namespace std::placeholders;
50
51
51
static const string sTpProduct("urn:av-openhome-org:service:Radio:1");
52
static const string sTpProduct("urn:av-openhome-org:service:Radio:1");
52
static const string sIdProduct("urn:av-openhome-org:serviceId:Radio");
53
static const string sIdProduct("urn:av-openhome-org:serviceId:Radio");
53
54
55
static string find_script(const string& icmd)
56
{
57
    if (path_isabsolute(icmd))
58
  return icmd;
59
60
    // Append the radio scripts dir to the PATH. Put at the end so
61
    // that the user can easily override a script by putting the
62
    // modified version in the PATH env variable
63
    const char *cp = getenv("PATH");
64
    if (!cp) //??
65
        cp = "";
66
    string PATH(cp);
67
    PATH = PATH + path_PATHsep() + path_cat(g_datadir, "radio_scripts");
68
    string cmd;
69
    if (ExecCmd::which(icmd, cmd, PATH.c_str())) {
70
        return cmd;
71
    } else {
72
        // Let the shell try to find it...
73
        return icmd;
74
    }
75
}
76
54
struct RadioMeta {
77
struct RadioMeta {
55
    RadioMeta(const string& t, const string& u, const string& au,
78
    RadioMeta(const string& t, const string& u, const string& au,
56
              const string& as, const string& ms, const string& ps)
79
              const string& as, const string& ms, const string& ps)
57
        : title(t), uri(u), artUri(au), dynArtUri(au) {
80
        : title(t), uri(u), artUri(au), dynArtUri(au) {
58
        if (!as.empty()) {
81
        if (!as.empty()) {
59
            stringToStrings(as, artScript);
82
            stringToStrings(as, artScript);
83
            artScript[0] = find_script(artScript[0]);
60
        }
84
        }
61
        if (!ms.empty()) {
85
        if (!ms.empty()) {
62
            stringToStrings(ms, metaScript);
86
            stringToStrings(ms, metaScript);
87
            metaScript[0] = find_script(metaScript[0]);
63
        }
88
        }
64
        preferScript = stringToBool(ps);
89
        preferScript = stringToBool(ps);
65
    }
90
    }
66
    string title;
91
    string title;
92
    // Static playlist URI (from config)
67
    string uri;
93
    string uri;
68
    string artUri;
69
    // Script to retrieve current art
94
    // Script to retrieve current art
70
    vector<string> artScript; 
95
    vector<string> artScript; 
71
    // Script to retrieve all metadata
96
    // Script to retrieve all metadata
72
    vector<string> metaScript;
97
    vector<string> metaScript;
98
    // Dynamic audio URI, fetched by the metadata script (overrides
99
    // uri, which will normally be empty if the metascript is used for
100
    // audio).
101
    string currentAudioUri;
102
    string artUri;
73
    // Keep values from script over mpd's (from icy)
103
    // Keep values from script over mpd's (from icy)
74
    bool preferScript;
104
    bool preferScript{true};
75
    // Time after which we should re-fire the metadata script
105
    // Time after which we should re-fire the metadata script
76
    time_t nextMetaScriptExecTime{0}; 
106
    time_t nextMetaScriptExecTime{0}; 
77
    string dynArtUri;
107
    string dynArtUri;
78
    string dynTitle;
108
    string dynTitle;
79
    string dynArtist;
109
    string dynArtist;
...
...
83
113
84
OHRadio::OHRadio(UpMpd *dev)
114
OHRadio::OHRadio(UpMpd *dev)
85
    : OHService(sTpProduct, sIdProduct, dev), m_active(false),
115
    : OHService(sTpProduct, sIdProduct, dev), m_active(false),
86
      m_id(0), m_ok(false)
116
      m_id(0), m_ok(false)
87
{
117
{
88
    // Need Python
118
    // Need Python for the radiopl playlist-to-audio-url script
89
    string pypath;
119
    string pypath;
90
    if (!ExecCmd::which("python2", pypath)) {
120
    if (!ExecCmd::which("python2", pypath)) {
91
        LOGINF("OHRadio: python2 not found, no radio service will be created\n");
121
        LOGINF("OHRadio: python2 not found, radio service will not work\n");
92
        return;
122
        return;
93
    }
123
    }
94
    if (!readRadios()) {
124
    if (!readRadios()) {
95
        LOGINF("OHRadio: readRadios() failed, no radio service will be created\n");
125
        LOGINF("OHRadio: readRadios() failed, radio service will not work\n");
96
        return;
126
        return;
97
    }
127
    }
98
    m_ok = true;
128
    m_ok = true;
99
    
129
    
100
    dev->addActionMapping(this, "Channel",
130
    dev->addActionMapping(this, "Channel",
...
...
136
    vector<string> allsubk = conf->getSubKeys_unsorted();
166
    vector<string> allsubk = conf->getSubKeys_unsorted();
137
    for (auto it = allsubk.begin(); it != allsubk.end(); it++) {
167
    for (auto it = allsubk.begin(); it != allsubk.end(); it++) {
138
        if (it->find("radio ") == 0) {
168
        if (it->find("radio ") == 0) {
139
            string uri, artUri, artScript, metaScript, preferScript;
169
            string uri, artUri, artScript, metaScript, preferScript;
140
            string title = it->substr(6);
170
            string title = it->substr(6);
141
            bool ok = conf->get("url", uri, *it);
171
            conf->get("url", uri, *it);
142
            conf->get("artUrl", artUri, *it);
172
            conf->get("artUrl", artUri, *it);
143
            conf->get("artScript", artScript, *it);
173
            conf->get("artScript", artScript, *it);
144
            trimstring(artScript, " \t\n\r");
174
            trimstring(artScript, " \t\n\r");
145
            conf->get("metaScript", metaScript, *it);
175
            conf->get("metaScript", metaScript, *it);
146
            trimstring(metaScript, " \t\n\r");
176
            trimstring(metaScript, " \t\n\r");
147
            conf->get("preferScript", preferScript, *it);
177
            conf->get("preferScript", preferScript, *it);
148
            trimstring(preferScript, " \t\n\r");
178
            trimstring(preferScript, " \t\n\r");
149
            if (ok && !uri.empty()) {
179
            if (!uri.empty() || !metaScript.empty()) {
150
                o_radios.push_back(RadioMeta(title, uri, artUri, artScript,
180
                o_radios.push_back(RadioMeta(title, uri, artUri, artScript,
151
                                             metaScript, preferScript));
181
                                             metaScript, preferScript));
152
                LOGDEB0("OHRadio::readRadios:RADIO: [" << title << "] uri ["
182
                LOGDEB("OHRadio::readRadios:RADIO: [" << title << "] uri ["
153
                        << uri << "] artUri [" << artUri << "]\n");
183
                       << uri << "] artUri [" << artUri << "] metaScript [" <<
184
                       metaScript << "] preferScript " << preferScript << endl);
154
            }
185
            }
155
        }
186
        }
156
    }
187
    }
157
}
188
}
158
189
...
...
209
    }
240
    }
210
    out = base64_encode(out1);
241
    out = base64_encode(out1);
211
    return true;
242
    return true;
212
}
243
}
213
244
245
void OHRadio::maybeExecMetaScript(RadioMeta& radio, MpdStatus &mpds)
246
{
247
    string seconds("-1");
248
    if (time(0) < radio.nextMetaScriptExecTime) {
249
        LOGDEB0("OHRadio::maybeExecMetaScript: next in " <<
250
                radio.nextMetaScriptExecTime - time(0) << endl);
251
        return;
252
    }
253
    if (mpds.state == MpdStatus::MPDS_PLAY) {
254
        seconds = SoapHelp::i2s(mpds.songelapsedms);
255
    }
256
    vector<string> args{radio.metaScript};
257
    args.push_back(seconds);
258
    string data;
259
    if (!ExecCmd::backtick(args, data)) {
260
        LOGERR("OHRadio::makestate: radio metascript failed\n");
261
        return;
262
    }
263
    LOGDEB0("OHRadio::makestate: metaScript got: [" << data << "]\n");
264
265
    // The data is in JSON format
266
    Json::Value decoded;
267
    try {
268
        istringstream input(data);
269
        input >> decoded;
270
    } catch (std::exception e) {
271
        LOGERR("OHRadio::makestate: Json decode failed for [" << data << "]");
272
        radio.nextMetaScriptExecTime = time(0) + 10;
273
        return;
274
    }
275
276
    radio.dynTitle = decoded.get("title", "").asString();
277
    radio.dynArtist = decoded.get("artist", "").asString();
278
    radio.dynArtUri = decoded.get("artUrl", "").asString();
279
    int reload = decoded.get("reload", 10).asInt();
280
    if (reload < 2) {
281
        reload = 2;
282
    }
283
    radio.nextMetaScriptExecTime = time(0) + reload;
284
    
285
    // If the script returns an audio uri, queue it to mpd
286
    string audioUri= decoded.get("audioUrl", "").asString();
287
    if (!audioUri.empty()) {
288
        vector<UpSong> queue;
289
        m_dev->m_mpdcli->getQueueData(queue);
290
        bool found = false;
291
        for (const auto& entry : queue) {
292
            if (entry.uri == audioUri) {
293
                found = true;
294
                break;
295
            }
296
        }
297
        if (!found) {
298
            UpSong song;
299
            song.album = radio.title;
300
            song.uri = audioUri;
301
            LOGDEB("INSERTING " << song.uri << endl);
302
            m_dev->m_mpdcli->single(false);
303
            m_dev->m_mpdcli->consume(true);
304
            if (m_dev->m_mpdcli->insert(audioUri, -1, song) < 0) {
305
                LOGERR("OHRadio::mkstate: mpd insert failed."<< " pos " <<
306
                       mpds.songpos << " uri " << audioUri << endl);
307
                return;
308
            }
309
        }
310
        // Have to do this else playing does not start, but this is
311
        // going to interfer with a user-initiated pause/stop state
312
        if (mpds.state != MpdStatus::MPDS_PLAY && !m_dev->m_mpdcli->play(0)) {
313
            LOGERR("OHRadio::mkstate: mpd play failed\n");
314
            return;
315
        }
316
        radio.currentAudioUri = audioUri;
317
    }
318
}
319
214
bool OHRadio::makestate(unordered_map<string, string>& st)
320
bool OHRadio::makestate(unordered_map<string, string>& st)
215
{
321
{
216
    st.clear();
322
    st.clear();
217
323
218
    MpdStatus mpds = m_dev->getMpdStatusNoUpdate();
324
    MpdStatus mpds = m_dev->getMpdStatusNoUpdate();
...
...
231
        // Some radios do not insert icy metadata in the stream, but rather
337
        // Some radios do not insert icy metadata in the stream, but rather
232
        // provide a script to retrieve it.
338
        // provide a script to retrieve it.
233
        bool nompddata = mpds.currentsong.title.empty() &&
339
        bool nompddata = mpds.currentsong.title.empty() &&
234
            mpds.currentsong.artist.empty();
340
            mpds.currentsong.artist.empty();
235
        if ((radio.preferScript || nompddata) && radio.metaScript.size()) {
341
        if ((radio.preferScript || nompddata) && radio.metaScript.size()) {
236
            if (time(0) > radio.nextMetaScriptExecTime) {
342
            maybeExecMetaScript(radio, mpds);
237
                string data;
238
                if (ExecCmd::backtick(radio.metaScript, data)) {
239
                    LOGDEB0("OHRadio::makestate: metaScript got: [" << data <<
240
                            "]\n");
241
                    // The data is in JSON format
242
                    try {
243
                        Json::Value decoded;
244
                        istringstream input(data);
245
                        input >> decoded;
246
                        radio.dynTitle = decoded.get("title", "").asString();
247
                        radio.dynArtist = decoded.get("artist", "").asString();
248
                        radio.dynArtUri = decoded.get("artUrl", "").asString();
249
                        int reload = decoded.get("reload", 10).asInt();
250
                        if (reload <= 3) {
251
                            reload = 3;
252
                        }
253
                        radio.nextMetaScriptExecTime = time(0) + reload;
254
                    } catch (std::exception e) {
255
                        LOGERR("OHRadio::makestate: Json decode failed for [" <<
256
                               data << "]");
257
                        radio.nextMetaScriptExecTime = time(0) + 10;
258
                    }
259
                }
260
            }
261
            mpds.currentsong.title = radio.dynTitle;
343
            mpds.currentsong.title = radio.dynTitle;
262
            mpds.currentsong.artist = radio.dynArtist;
344
            mpds.currentsong.artist = radio.dynArtist;
263
        }
345
        }
264
346
265
        // Some radios provide a url to the art for the current song. 
347
        // Some radios provide a url to the art for the current song. 
...
...
303
    }
385
    }
304
}
386
}
305
387
306
int OHRadio::setPlaying()
388
int OHRadio::setPlaying()
307
{
389
{
308
    if (m_id > o_radios.size() || o_radios[m_id].uri.empty()) {
390
    if (m_id > o_radios.size()) {
309
        LOGERR("OHRadio::setPlaying: called with bad id (" << m_id <<
391
        LOGERR("OHRadio::setPlaying: called with bad id (" << m_id << ")\n");
310
               ") or empty preset uri [" << o_radios[m_id].uri << "]\n");
311
        return UPNP_E_INTERNAL_ERROR;
392
        return UPNP_E_INTERNAL_ERROR;
312
    }
393
    }
313
394
314
    RadioMeta& radio = o_radios[m_id];
395
    RadioMeta& radio = o_radios[m_id];
315
    radio.nextMetaScriptExecTime = 0;
396
    radio.nextMetaScriptExecTime = 0;
397
398
    if (radio.uri.empty() && radio.metaScript.empty()) {
399
        LOGERR("OHRadio::setPlaying: both URI and metascript are empty !\n");
400
        return UPNP_E_INVALID_PARAM;
401
    }
402
403
    if (radio.uri.empty()) {
404
        // We count on the metascript to also return an audio URI,
405
        // which will be sent to MPD during makestate().
406
        radio.currentAudioUri.clear();
407
        m_dev->m_mpdcli->clearQueue();
408
        return UPNP_E_SUCCESS;
409
    }
316
    
410
    
317
    string cmdpath = path_cat(g_datadir, "rdpl2stream");
411
    string cmdpath = path_cat(g_datadir, "rdpl2stream");
318
    cmdpath = path_cat(cmdpath, "fetchStream.py");
412
    cmdpath = path_cat(cmdpath, "fetchStream.py");
319
413
320
    // Execute the playlist parser
414
    // Execute the playlist parser
...
...
555
            out += "</Uri><Metadata>";
649
            out += "</Uri><Metadata>";
556
            out += SoapHelp::xmlQuote(meta);
650
            out += SoapHelp::xmlQuote(meta);
557
            out += "</Metadata></Entry>";
651
            out += "</Metadata></Entry>";
558
        }
652
        }
559
        out += "</ChannelList>";
653
        out += "</ChannelList>";
560
        LOGDEB("OHRadio::readList: out: [" << out << "]" << endl);
654
        LOGDEB0("OHRadio::readList: out: [" << out << "]" << endl);
561
        data.addarg("ChannelList", out);
655
        data.addarg("ChannelList", out);
562
    }
656
    }
563
    return ok ? UPNP_E_SUCCESS : UPNP_E_INTERNAL_ERROR;
657
    return ok ? UPNP_E_SUCCESS : UPNP_E_INTERNAL_ERROR;
564
}
658
}
565
659