|
a/src/cdplugins/tidal.cxx |
|
b/src/cdplugins/tidal.cxx |
|
... |
|
... |
44 |
using json = nlohmann::json;
|
44 |
using json = nlohmann::json;
|
45 |
using namespace UPnPProvider;
|
45 |
using namespace UPnPProvider;
|
46 |
|
46 |
|
47 |
class StreamHandle {
|
47 |
class StreamHandle {
|
48 |
public:
|
48 |
public:
|
49 |
StreamHandle() : avio(0), offset(0) {
|
49 |
StreamHandle(Tidal::Internal *plg) : avio(0), offset(0) {
|
50 |
}
|
50 |
}
|
51 |
~StreamHandle() {
|
51 |
~StreamHandle() {
|
52 |
clear();
|
52 |
clear();
|
53 |
}
|
53 |
}
|
54 |
void clear() {
|
54 |
void clear() {
|
|
|
55 |
plg = 0;
|
55 |
if (avio)
|
56 |
if (avio)
|
56 |
avio_close(avio);
|
57 |
avio_close(avio);
|
57 |
path.clear();
|
58 |
path.clear();
|
58 |
media_url.clear();
|
59 |
media_url.clear();
|
59 |
len = 0;
|
60 |
len = 0;
|
60 |
offset = 0;
|
61 |
offset = 0;
|
61 |
opentime = 0;
|
62 |
opentime = 0;
|
62 |
}
|
63 |
}
|
|
|
64 |
Tidal::Internal *plg;
|
63 |
AVIOContext *avio;
|
65 |
AVIOContext *avio;
|
64 |
string path;
|
66 |
string path;
|
65 |
string media_url;
|
67 |
string media_url;
|
66 |
int len;
|
68 |
long long len;
|
67 |
off_t offset;
|
69 |
off_t offset;
|
68 |
time_t opentime;
|
70 |
time_t opentime;
|
69 |
};
|
71 |
};
|
70 |
|
72 |
|
71 |
class Tidal::Internal {
|
73 |
class Tidal::Internal {
|
72 |
public:
|
74 |
public:
|
73 |
Internal(Tidal *tidal, const vector<string>& pth, const string& hst,
|
75 |
Internal(Tidal *tidal, const vector<string>& pth, const string& hst,
|
74 |
int prt, const string& pp)
|
76 |
int prt, const string& pp)
|
75 |
: plg(tidal), path(pth), host(hst), port(prt), pathprefix(pp), kbs(0),
|
77 |
: plg(tidal), path(pth), host(hst), port(prt), pathprefix(pp), kbs(0),
|
|
|
78 |
laststream(this),
|
76 |
mhd(0) { }
|
79 |
mhd(0) { }
|
77 |
|
80 |
|
78 |
bool maybeStartCmd(const string&);
|
81 |
bool maybeStartCmd(const string&);
|
79 |
string get_media_url(const std::string& path);
|
82 |
string get_media_url(const std::string& path);
|
80 |
|
83 |
|
|
... |
|
... |
108 |
StreamHandle laststream;
|
111 |
StreamHandle laststream;
|
109 |
// When using microhttpd
|
112 |
// When using microhttpd
|
110 |
struct MHD_Daemon *mhd;
|
113 |
struct MHD_Daemon *mhd;
|
111 |
};
|
114 |
};
|
112 |
|
115 |
|
113 |
// Microhttpd connection handler. This is only used when working with
|
116 |
// Parse range header.
|
114 |
// HTTP/FLAC. We re-build the complete url + query string
|
117 |
static void parseRanges(const string& ranges, vector<pair<int,int> >& oranges)
|
|
|
118 |
{
|
|
|
119 |
oranges.clear();
|
|
|
120 |
string::size_type pos = ranges.find("bytes=");
|
|
|
121 |
if (pos == string::npos) {
|
|
|
122 |
return;
|
|
|
123 |
}
|
|
|
124 |
pos += 6;
|
|
|
125 |
bool done = false;
|
|
|
126 |
while(!done) {
|
|
|
127 |
string::size_type dash = ranges.find('-', pos);
|
|
|
128 |
string::size_type comma = ranges.find(',', pos);
|
|
|
129 |
string firstPart = dash != string::npos ?
|
|
|
130 |
ranges.substr(pos, dash-pos) : "";
|
|
|
131 |
int start = firstPart.empty() ? 0 : atoi(firstPart.c_str());
|
|
|
132 |
string secondPart = dash != string::npos ?
|
|
|
133 |
ranges.substr(dash+1, comma != string::npos ?
|
|
|
134 |
comma-dash-1 : string::npos) : "";
|
|
|
135 |
int fin = secondPart.empty() ? -1 : atoi(firstPart.c_str());
|
|
|
136 |
pair<int,int> nrange(start,fin);
|
|
|
137 |
oranges.push_back(nrange);
|
|
|
138 |
if (comma != string::npos) {
|
|
|
139 |
pos = comma + 1;
|
|
|
140 |
}
|
|
|
141 |
done = comma == string::npos;
|
|
|
142 |
}
|
|
|
143 |
}
|
|
|
144 |
|
|
|
145 |
static void ContentReaderFreeCallback(void *cls)
|
|
|
146 |
{
|
|
|
147 |
StreamHandle *hdl = (StreamHandle*)cls;
|
|
|
148 |
delete hdl;
|
|
|
149 |
}
|
|
|
150 |
|
|
|
151 |
static ssize_t
|
|
|
152 |
data_generator(void *cls, uint64_t pos, char *buf, size_t max)
|
|
|
153 |
{
|
|
|
154 |
StreamHandle *hdl = (StreamHandle *)cls;
|
|
|
155 |
LOGDEB1("data_generator: pos " << pos << " max " << max << endl);
|
|
|
156 |
return hdl->plg->read(cls, buf, max);
|
|
|
157 |
}
|
|
|
158 |
|
|
|
159 |
static const char *ValueKindToCp(enum MHD_ValueKind kind)
|
|
|
160 |
{
|
|
|
161 |
switch (kind) {
|
|
|
162 |
case MHD_RESPONSE_HEADER_KIND: return "Response header";
|
|
|
163 |
case MHD_HEADER_KIND: return "HTTP header";
|
|
|
164 |
case MHD_COOKIE_KIND: return "Cookies";
|
|
|
165 |
case MHD_POSTDATA_KIND: return "POST data";
|
|
|
166 |
case MHD_GET_ARGUMENT_KIND: return "GET (URI) arguments";
|
|
|
167 |
case MHD_FOOTER_KIND: return "HTTP footer";
|
|
|
168 |
default: return "Unknown";
|
|
|
169 |
}
|
|
|
170 |
}
|
|
|
171 |
|
|
|
172 |
static int print_out_key (void *cls, enum MHD_ValueKind kind,
|
|
|
173 |
const char *key, const char *value)
|
|
|
174 |
{
|
|
|
175 |
LOGDEB(ValueKindToCp(kind) << ": " << key << " -> " << value << endl);
|
|
|
176 |
return MHD_YES;
|
|
|
177 |
}
|
|
|
178 |
|
|
|
179 |
// Microhttpd connection handler. We re-build the complete url + query
|
115 |
// (&trackid=value), use this to retrieve a Tidal HTTP URL, and redirect to it.
|
180 |
// string (&trackid=value), use this to retrieve a Tidal URL, and
|
|
|
181 |
// either redirect to it if it is http or manage the reading.
|
116 |
static int answer_to_connection(void *cls, struct MHD_Connection *connection,
|
182 |
static int answer_to_connection(void *cls, struct MHD_Connection *connection,
|
117 |
const char *url,
|
183 |
const char *url,
|
118 |
const char *method, const char *version,
|
184 |
const char *method, const char *version,
|
119 |
const char *upload_data,
|
185 |
const char *upload_data,
|
120 |
size_t *upload_data_size, void **con_cls)
|
186 |
size_t *upload_data_size, void **con_cls)
|
|
... |
|
... |
146 |
if (media_url.empty()) {
|
212 |
if (media_url.empty()) {
|
147 |
LOGERR("answer_to_connection: no media_uri for: " << url << endl);
|
213 |
LOGERR("answer_to_connection: no media_uri for: " << url << endl);
|
148 |
return MHD_NO;
|
214 |
return MHD_NO;
|
149 |
}
|
215 |
}
|
150 |
|
216 |
|
|
|
217 |
if (media_url.find("http") == 0) {
|
151 |
LOGDEB("Tidal: redirecting to " << media_url << endl);
|
218 |
LOGDEB("Tidal: redirecting to " << media_url << endl);
|
152 |
|
219 |
|
153 |
static char data[] = "<html><body></body></html>";
|
220 |
static char data[] = "<html><body></body></html>";
|
|
|
221 |
struct MHD_Response *response =
|
|
|
222 |
MHD_create_response_from_buffer(strlen(data), data,
|
|
|
223 |
MHD_RESPMEM_PERSISTENT);
|
|
|
224 |
if (response == NULL) {
|
|
|
225 |
LOGERR("answer_to_connection: could not create response" << endl);
|
|
|
226 |
return MHD_NO;
|
|
|
227 |
}
|
|
|
228 |
MHD_add_response_header (response, "Location", media_url.c_str());
|
|
|
229 |
int ret = MHD_queue_response(connection, 302, response);
|
|
|
230 |
MHD_destroy_response(response);
|
|
|
231 |
return ret;
|
|
|
232 |
}
|
|
|
233 |
|
|
|
234 |
// rtmp stream, read and send.
|
|
|
235 |
MHD_get_connection_values(connection, MHD_HEADER_KIND, &print_out_key, 0);
|
|
|
236 |
const char* rangeh = MHD_lookup_connection_value(connection,
|
|
|
237 |
MHD_HEADER_KIND, "range");
|
|
|
238 |
vector<pair<int,int> > ranges;
|
|
|
239 |
if (rangeh) {
|
|
|
240 |
LOGDEB("answer_to_connection: got ranges\n");
|
|
|
241 |
parseRanges(rangeh, ranges);
|
|
|
242 |
}
|
|
|
243 |
|
|
|
244 |
// open will ccall get_media_url again but this is ok because the
|
|
|
245 |
// value is cached
|
|
|
246 |
StreamHandle *hdl = (StreamHandle*)me->open(path);
|
|
|
247 |
|
|
|
248 |
long long size = hdl->len;
|
|
|
249 |
LOGDEB("answer_to_connection: stream size: " << size << endl);
|
|
|
250 |
|
|
|
251 |
if (ranges.size()) {
|
|
|
252 |
if (ranges[0].second != -1) {
|
|
|
253 |
size = ranges[0].second - ranges[0].first + 1;
|
|
|
254 |
}
|
|
|
255 |
me->seek(hdl, ranges[0].first, 0);
|
|
|
256 |
}
|
|
|
257 |
|
154 |
struct MHD_Response *response =
|
258 |
struct MHD_Response *response =
|
155 |
MHD_create_response_from_buffer(strlen(data), data,
|
259 |
MHD_create_response_from_callback(size, 100*1024, &data_generator,
|
156 |
MHD_RESPMEM_PERSISTENT);
|
260 |
hdl, ContentReaderFreeCallback);
|
157 |
if (response == NULL) {
|
261 |
if (response == NULL) {
|
158 |
LOGERR("answer_to_connection: could not create response" << endl);
|
262 |
LOGERR("httpgate: answer: could not create response" << endl);
|
159 |
return MHD_NO;
|
263 |
return MHD_NO;
|
160 |
}
|
264 |
}
|
|
|
265 |
MHD_add_response_header (response, "Content-Type", "application/flv");
|
|
|
266 |
if (size > 0) {
|
|
|
267 |
string ssize;
|
|
|
268 |
lltodecstr(size, ssize);
|
161 |
MHD_add_response_header (response, "Location", media_url.c_str());
|
269 |
MHD_add_response_header (response, "Content-Length", ssize.c_str());
|
|
|
270 |
}
|
|
|
271 |
MHD_add_response_header (response, "Accept-Ranges", "bytes");
|
162 |
int ret = MHD_queue_response(connection, 302, response);
|
272 |
int ret = MHD_queue_response (connection, MHD_HTTP_OK, response);
|
163 |
MHD_destroy_response(response);
|
273 |
MHD_destroy_response(response);
|
164 |
return ret;
|
274 |
return ret;
|
165 |
}
|
275 |
}
|
166 |
|
276 |
|
167 |
static int accept_policy(void *, const struct sockaddr* sa, socklen_t addrlen)
|
277 |
static int accept_policy(void *, const struct sockaddr* sa, socklen_t addrlen)
|
|
... |
|
... |
183 |
if (!conf || !conf->get("tidalquality", tidalquality)) {
|
293 |
if (!conf || !conf->get("tidalquality", tidalquality)) {
|
184 |
LOGERR("Tidal: can't get parameter 'tidalquality' from config\n");
|
294 |
LOGERR("Tidal: can't get parameter 'tidalquality' from config\n");
|
185 |
return false;
|
295 |
return false;
|
186 |
}
|
296 |
}
|
187 |
|
297 |
|
188 |
bool using_miniserver = tidalquality.compare("lossless");
|
|
|
189 |
|
298 |
|
190 |
if (using_miniserver) {
|
299 |
if (tidalquality.compare("lossless")) {
|
191 |
av_register_all();
|
300 |
av_register_all();
|
192 |
avformat_network_init();
|
301 |
avformat_network_init();
|
193 |
VirtualDir::FileOps ops;
|
302 |
}
|
194 |
ops.getinfo = bind(&Tidal::Internal::getinfo, this, _1, _2);
|
|
|
195 |
ops.open = bind(&Tidal::Internal::open, this, _1);
|
|
|
196 |
ops.read = bind(&Tidal::Internal::read, this, _1, _2, _3);
|
|
|
197 |
ops.seek = bind(&Tidal::Internal::seek, this, _1, _2, _3);
|
|
|
198 |
ops.close = bind(&Tidal::Internal::close, this, _1);
|
|
|
199 |
plg->m_services->setfileops(plg, plg->m_services->getpathprefix(plg),
|
|
|
200 |
ops);
|
|
|
201 |
} else {
|
|
|
202 |
port = 49149;
|
303 |
port = 49149;
|
203 |
string sport;
|
304 |
string sport;
|
204 |
if (conf->get("tidalmicrohttpport", sport)) {
|
305 |
if (conf->get("tidalmicrohttpport", sport)) {
|
205 |
port = atoi(sport.c_str());
|
306 |
port = atoi(sport.c_str());
|
206 |
}
|
307 |
}
|
207 |
mhd = MHD_start_daemon(
|
308 |
mhd = MHD_start_daemon(
|
208 |
MHD_USE_THREAD_PER_CONNECTION,
|
309 |
MHD_USE_THREAD_PER_CONNECTION,
|
209 |
//MHD_USE_SELECT_INTERNALLY,
|
310 |
//MHD_USE_SELECT_INTERNALLY,
|
210 |
port,
|
311 |
port,
|
211 |
/* Accept policy callback and arg */
|
312 |
/* Accept policy callback and arg */
|
212 |
accept_policy, NULL,
|
313 |
accept_policy, NULL,
|
213 |
/* handler and arg */
|
314 |
/* handler and arg */
|
214 |
&answer_to_connection, this,
|
315 |
&answer_to_connection, this,
|
215 |
MHD_OPTION_END);
|
316 |
MHD_OPTION_END);
|
216 |
if (nullptr == mhd) {
|
317 |
if (nullptr == mhd) {
|
217 |
LOGERR("Tidal: MHD_start_daemon failed\n");
|
318 |
LOGERR("Tidal: MHD_start_daemon failed\n");
|
218 |
return false;
|
319 |
return false;
|
219 |
}
|
|
|
220 |
}
|
320 |
}
|
221 |
|
321 |
|
222 |
string pythonpath = string("PYTHONPATH=") +
|
322 |
string pythonpath = string("PYTHONPATH=") +
|
223 |
path_cat(g_datadir, "cdplugins/pycommon");
|
323 |
path_cat(g_datadir, "cdplugins/pycommon");
|
224 |
string configname = string("UPMPD_CONFIG=") + g_configfilename;
|
324 |
string configname = string("UPMPD_CONFIG=") + g_configfilename;
|
|
... |
|
... |
329 |
auto result = avio_open(&avio, media_url.c_str(), AVIO_FLAG_READ);
|
429 |
auto result = avio_open(&avio, media_url.c_str(), AVIO_FLAG_READ);
|
330 |
if (result != 0) {
|
430 |
if (result != 0) {
|
331 |
LOGERR("Tidal: avio_open failed for [" << media_url << "]\n");
|
431 |
LOGERR("Tidal: avio_open failed for [" << media_url << "]\n");
|
332 |
return nullptr;
|
432 |
return nullptr;
|
333 |
}
|
433 |
}
|
334 |
StreamHandle *hdl = new StreamHandle;
|
434 |
StreamHandle *hdl = new StreamHandle(this);
|
335 |
hdl->path = path;
|
435 |
hdl->path = path;
|
336 |
hdl->media_url = media_url;
|
436 |
hdl->media_url = media_url;
|
337 |
hdl->avio = avio;
|
437 |
hdl->avio = avio;
|
|
|
438 |
LOGDEB("Tidal::open: avio_size returns " << avio_size(avio) << endl);
|
|
|
439 |
hdl->len = avio_size(avio);
|
|
|
440 |
if (hdl->len < 0)
|
|
|
441 |
hdl->len = -1;
|
338 |
hdl->opentime = time(0);
|
442 |
hdl->opentime = time(0);
|
339 |
return hdl;
|
443 |
return hdl;
|
340 |
} else {
|
444 |
} else {
|
341 |
// This should not happen.
|
445 |
// This should not happen.
|
342 |
LOGERR("Tidal::open: called for http stream ??\n");
|
446 |
LOGERR("Tidal::open: called for http stream ??\n");
|
|
... |
|
... |
344 |
}
|
448 |
}
|
345 |
}
|
449 |
}
|
346 |
|
450 |
|
347 |
int Tidal::Internal::read(void *_hdl, char* buf, size_t cnt)
|
451 |
int Tidal::Internal::read(void *_hdl, char* buf, size_t cnt)
|
348 |
{
|
452 |
{
|
349 |
LOGDEB("Tidal::read: " << cnt << endl);
|
453 |
LOGDEB1("Tidal::read: " << cnt << endl);
|
350 |
if (!_hdl)
|
454 |
if (!_hdl)
|
351 |
return -1;
|
455 |
return -1;
|
352 |
|
456 |
|
353 |
// The pupnp http code has a default 1MB buffer size which is much
|
457 |
// The pupnp http code has a default 1MB buffer size which is much
|
354 |
// too big for us (too slow, esp. because tidal will stall).
|
458 |
// too big for us (too slow, esp. because tidal will stall).
|
|
... |
|
... |
362 |
auto totread = avio_read(hdl->avio, (unsigned char *)buf, cnt);
|
466 |
auto totread = avio_read(hdl->avio, (unsigned char *)buf, cnt);
|
363 |
if (totread <= 0) {
|
467 |
if (totread <= 0) {
|
364 |
LOGERR("Tidal: avio_read(" << cnt << ") failed\n");
|
468 |
LOGERR("Tidal: avio_read(" << cnt << ") failed\n");
|
365 |
return -1;
|
469 |
return -1;
|
366 |
}
|
470 |
}
|
367 |
LOGDEB("Tidal::read: total read: " << totread << endl);
|
471 |
LOGDEB1("Tidal::read: total read: " << totread << endl);
|
368 |
hdl->offset += totread;
|
472 |
hdl->offset += totread;
|
369 |
return int(totread);
|
473 |
return int(totread);
|
370 |
} else {
|
474 |
} else {
|
371 |
LOGERR("Tidal::read: no handle\n");
|
475 |
LOGERR("Tidal::read: no handle\n");
|
372 |
return -1;
|
476 |
return -1;
|
|
... |
|
... |
467 |
{
|
571 |
{
|
468 |
LOGDEB("Tidal::browse\n");
|
572 |
LOGDEB("Tidal::browse\n");
|
469 |
if (!m->maybeStartCmd("browse")) {
|
573 |
if (!m->maybeStartCmd("browse")) {
|
470 |
return -1;
|
574 |
return -1;
|
471 |
}
|
575 |
}
|
|
|
576 |
string sbflg;
|
|
|
577 |
switch (flg) {
|
|
|
578 |
case CDPlugin::BFMeta:
|
|
|
579 |
sbflg = "meta";
|
|
|
580 |
break;
|
|
|
581 |
case CDPlugin::BFChildren:
|
|
|
582 |
default:
|
|
|
583 |
sbflg = "children";
|
|
|
584 |
break;
|
|
|
585 |
}
|
|
|
586 |
|
472 |
unordered_map<string, string> res;
|
587 |
unordered_map<string, string> res;
|
473 |
if (!m->cmd.callproc("browse", {{"objid", objid}}, res)) {
|
588 |
if (!m->cmd.callproc("browse", {{"objid", objid}, {"flag", sbflg}}, res)) {
|
474 |
LOGERR("Tidal::browse: slave failure\n");
|
589 |
LOGERR("Tidal::browse: slave failure\n");
|
475 |
return -1;
|
590 |
return -1;
|
476 |
}
|
591 |
}
|
477 |
|
592 |
|
478 |
auto it = res.find("entries");
|
593 |
auto it = res.find("entries");
|