|
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 |
|