1 /*
2 * tvheadend, bouquets
3 * Copyright (C) 2014 Jaroslav Kysela
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "tvheadend.h"
20 #include "settings.h"
21 #include "access.h"
22 #include "bouquet.h"
23 #include "service.h"
24 #include "channels.h"
25 #include "service_mapper.h"
26 #include "download.h"
27
28 typedef struct bouquet_download {
29 bouquet_t *bq;
30 download_t download;
31 mtimer_t timer;
32 } bouquet_download_t;
33
34 bouquet_tree_t bouquets;
35
36 static void bouquet_remove_service(bouquet_t *bq, service_t *s, int delconf);
37 static void bouquet_download_trigger(bouquet_t *bq);
38 static void bouquet_download_stop(void *aux);
39 static int bouquet_download_process(void *aux, const char *last_url, const char *host_url, char *data, size_t len);
40
41 /**
42 *
43 */
44 static int
_bq_cmp(const void * a,const void * b)45 _bq_cmp(const void *a, const void *b)
46 {
47 return strcmp(((bouquet_t *)a)->bq_src ?: "", ((bouquet_t *)b)->bq_src ?: "");
48 }
49
50 /**
51 *
52 */
53 static void
bouquet_free(bouquet_t * bq)54 bouquet_free(bouquet_t *bq)
55 {
56 bouquet_download_t *bqd;
57
58 idnode_save_check(&bq->bq_id, 1);
59 idnode_unlink(&bq->bq_id);
60
61 if ((bqd = bq->bq_download) != NULL) {
62 bouquet_download_stop(bqd);
63 download_done(&bqd->download);
64 free(bqd);
65 }
66
67 idnode_set_free(bq->bq_active_services);
68 idnode_set_free(bq->bq_services);
69 htsmsg_destroy(bq->bq_services_waiting);
70 free((char *)bq->bq_chtag_waiting);
71 free(bq->bq_name);
72 free(bq->bq_ext_url);
73 free(bq->bq_src);
74 free(bq->bq_comment);
75 free(bq);
76 }
77
78 /**
79 *
80 */
81 bouquet_t *
bouquet_create(const char * uuid,htsmsg_t * conf,const char * name,const char * src)82 bouquet_create(const char *uuid, htsmsg_t *conf,
83 const char *name, const char *src)
84 {
85 bouquet_t *bq, *bq2;
86 bouquet_download_t *bqd;
87 char buf[128], ubuf[UUID_HEX_SIZE];
88 int i;
89
90 lock_assert(&global_lock);
91
92 bq = calloc(1, sizeof(bouquet_t));
93 bq->bq_services = idnode_set_create(1);
94 bq->bq_active_services = idnode_set_create(1);
95 bq->bq_ext_url_period = 60;
96 bq->bq_mapencrypted = 1;
97 bq->bq_mapradio = 1;
98 bq->bq_maptoch = 1;
99 bq->bq_chtag = 1;
100
101 if (idnode_insert(&bq->bq_id, uuid, &bouquet_class, 0)) {
102 if (uuid)
103 tvherror(LS_BOUQUET, "invalid uuid '%s'", uuid);
104 bouquet_free(bq);
105 return NULL;
106 }
107
108 if (conf) {
109 bq->bq_in_load = 1;
110 idnode_load(&bq->bq_id, conf);
111 bq->bq_in_load = 0;
112 if (!htsmsg_get_bool(conf, "shield", &i) && i)
113 bq->bq_shield = 1;
114 }
115
116 if (name) {
117 free(bq->bq_name);
118 bq->bq_name = strdup(name);
119 }
120
121 if (src) {
122 free(bq->bq_src);
123 bq->bq_src = strdup(src);
124 }
125
126 if (bq->bq_ext_url) {
127 if (bq->bq_src && strncmp(bq->bq_src, "exturl://", 9) != 0) {
128 free(bq->bq_ext_url);
129 bq->bq_ext_url = NULL;
130 } else {
131 free(bq->bq_src);
132 snprintf(buf, sizeof(buf), "exturl://%s", idnode_uuid_as_str(&bq->bq_id, ubuf));
133 bq->bq_src = strdup(buf);
134 bq->bq_download = bqd = calloc(1, sizeof(*bqd));
135 bqd->bq = bq;
136 download_init(&bqd->download, LS_BOUQUET);
137 bqd->download.process = bouquet_download_process;
138 bqd->download.stop = bouquet_download_stop;
139 bouquet_change_comment(bq, bq->bq_ext_url, 0);
140 }
141 }
142
143 bq2 = RB_INSERT_SORTED(&bouquets, bq, bq_link, _bq_cmp);
144 if (bq2) {
145 tvherror(LS_BOUQUET, "found duplicate source id: '%s', remove duplicate config", bq->bq_src);
146 bouquet_free(bq);
147 return NULL;
148 }
149
150 bq->bq_saveflag = 1;
151
152 if (bq->bq_ext_url)
153 bouquet_download_trigger(bq);
154
155 return bq;
156 }
157
158 /**
159 *
160 */
161 static void
bouquet_destroy(bouquet_t * bq)162 bouquet_destroy(bouquet_t *bq)
163 {
164 if (!bq)
165 return;
166
167 RB_REMOVE(&bouquets, bq, bq_link);
168
169 bouquet_free(bq);
170 }
171
172 /**
173 *
174 */
175 void
bouquet_destroy_by_service(service_t * t,int delconf)176 bouquet_destroy_by_service(service_t *t, int delconf)
177 {
178 bouquet_t *bq;
179 service_lcn_t *sl;
180
181 lock_assert(&global_lock);
182
183 RB_FOREACH(bq, &bouquets, bq_link)
184 if (idnode_set_exists(bq->bq_services, &t->s_id))
185 bouquet_remove_service(bq, t, delconf);
186 while ((sl = LIST_FIRST(&t->s_lcns)) != NULL) {
187 LIST_REMOVE(sl, sl_link);
188 free(sl);
189 }
190 }
191
192 /**
193 *
194 */
195 void
bouquet_destroy_by_channel_tag(channel_tag_t * ct)196 bouquet_destroy_by_channel_tag(channel_tag_t *ct)
197 {
198 bouquet_t *bq;
199
200 lock_assert(&global_lock);
201
202 RB_FOREACH(bq, &bouquets, bq_link)
203 if (bq->bq_chtag_ptr == ct)
204 bq->bq_chtag_ptr = NULL;
205 }
206
207 /*
208 *
209 */
210 bouquet_t *
bouquet_find_by_source(const char * name,const char * src,int create)211 bouquet_find_by_source(const char *name, const char *src, int create)
212 {
213 bouquet_t *bq;
214 bouquet_t bqs;
215
216 assert(src);
217
218 lock_assert(&global_lock);
219
220 bqs.bq_src = (char *)src;
221 bq = RB_FIND(&bouquets, &bqs, bq_link, _bq_cmp);
222 if (bq) {
223 if (name && *name && bq->bq_name && strcmp(name, bq->bq_name)) {
224 tvhwarn(LS_BOUQUET, "bouquet name '%s' changed to '%s'", bq->bq_name ?: "", name);
225 free(bq->bq_name);
226 bq->bq_name = strdup(name);
227 idnode_changed(&bq->bq_id);
228 }
229 return bq;
230 }
231 if (create && name) {
232 bq = bouquet_create(NULL, NULL, name, src);
233 tvhinfo(LS_BOUQUET, "new bouquet '%s'", name);
234 return bq;
235 }
236 return NULL;
237 }
238
239 /*
240 *
241 */
242 static channel_tag_t *
bouquet_tag(bouquet_t * bq,int create)243 bouquet_tag(bouquet_t *bq, int create)
244 {
245 channel_tag_t *ct;
246 char buf[128];
247
248 assert(!bq->bq_in_load);
249 if (bq->bq_chtag_waiting) {
250 bq->bq_chtag_ptr = channel_tag_find_by_uuid(bq->bq_chtag_waiting);
251 free((char *)bq->bq_chtag_waiting);
252 bq->bq_chtag_waiting = NULL;
253 }
254 if (bq->bq_chtag_ptr)
255 return bq->bq_chtag_ptr;
256 snprintf(buf, sizeof(buf), "*** %s", bq->bq_name ?: "???");
257 ct = channel_tag_find_by_name(buf, create);
258 if (ct) {
259 bq->bq_chtag_ptr = ct;
260 idnode_changed(&bq->bq_id);
261 }
262 return ct;
263 }
264
265 /*
266 *
267 */
268 static int
noname(const char * s)269 noname(const char *s)
270 {
271 if (!s)
272 return 1;
273 while (*s) {
274 if (*s > ' ')
275 return 0;
276 s++;
277 }
278 return 1;
279 }
280
281 /*
282 *
283 */
284 static void
bouquet_map_channel(bouquet_t * bq,service_t * t)285 bouquet_map_channel(bouquet_t *bq, service_t *t)
286 {
287 channel_t *ch = NULL;
288 idnode_list_mapping_t *ilm;
289 static service_mapper_conf_t sm_conf = {
290 .check_availability = 0,
291 .encrypted = 1,
292 .merge_same_name = 0,
293 .type_tags = 0,
294 .provider_tags = 0,
295 .network_tags = 0
296 };
297
298 if (!t->s_enabled)
299 return;
300 if (!bq->bq_mapradio && service_is_radio(t))
301 return;
302 if (!bq->bq_mapnolcn &&
303 bouquet_get_channel_number(bq, t) <= 0)
304 return;
305 if (!bq->bq_mapnoname && noname(service_get_channel_name(t)))
306 return;
307 if (!bq->bq_mapencrypted && service_is_encrypted(t))
308 return;
309 LIST_FOREACH(ilm, &t->s_channels, ilm_in1_link)
310 if (((channel_t *)ilm->ilm_in2)->ch_bouquet == bq)
311 break;
312 if (!ilm) {
313 sm_conf.encrypted = bq->bq_mapencrypted;
314 sm_conf.merge_same_name = bq->bq_mapmergename;
315 sm_conf.type_tags = bq->bq_chtag_type_tags;
316 sm_conf.provider_tags = bq->bq_chtag_provider_tags;
317 sm_conf.network_tags = bq->bq_chtag_network_tags;
318 ch = service_mapper_process(&sm_conf, t, bq);
319 } else
320 ch = (channel_t *)ilm->ilm_in2;
321 if (ch && bq->bq_chtag)
322 if (channel_tag_map(bouquet_tag(bq, 1), ch, ch))
323 idnode_changed(&ch->ch_id);
324 }
325
326 /*
327 *
328 */
329 void
bouquet_add_service(bouquet_t * bq,service_t * s,uint64_t lcn,const char * tag)330 bouquet_add_service(bouquet_t *bq, service_t *s, uint64_t lcn, const char *tag)
331 {
332 service_lcn_t *tl;
333 idnode_list_mapping_t *ilm;
334
335 lock_assert(&global_lock);
336
337 if (!bq->bq_enabled)
338 return;
339
340 if (!idnode_set_exists(bq->bq_services, &s->s_id)) {
341 tvhtrace(LS_BOUQUET, "add service %s [%p] to %s lcn %"PRIu64"(.%"PRIu64")",
342 s->s_nicename, s, bq->bq_name ?: "<unknown>",
343 lcn / CHANNEL_SPLIT, lcn % CHANNEL_SPLIT);
344 idnode_set_add(bq->bq_services, &s->s_id, NULL, NULL);
345 bq->bq_saveflag = 1;
346 }
347
348 LIST_FOREACH(tl, &s->s_lcns, sl_link)
349 if (tl->sl_bouquet == bq)
350 break;
351
352 if (!tl) {
353 tl = calloc(1, sizeof(*tl));
354 tl->sl_bouquet = bq;
355 LIST_INSERT_HEAD(&s->s_lcns, tl, sl_link);
356 bq->bq_saveflag = 1;
357 } else {
358 if (tl->sl_lcn != lcn)
359 bq->bq_saveflag = 1;
360 }
361 if (lcn != tl->sl_lcn) {
362 tl->sl_lcn = lcn;
363 LIST_FOREACH(ilm, &s->s_channels, ilm_in1_link)
364 idnode_notify_changed(ilm->ilm_in2);
365 }
366 tl->sl_seen = 1;
367
368 if (bq->bq_enabled && bq->bq_maptoch)
369 bouquet_map_channel(bq, s);
370
371 if (!bq->bq_in_load &&
372 !idnode_set_exists(bq->bq_active_services, &s->s_id))
373 idnode_set_add(bq->bq_active_services, &s->s_id, NULL, NULL);
374 }
375
376 /*
377 *
378 */
379 static void
bouquet_unmap_channel(bouquet_t * bq,service_t * t)380 bouquet_unmap_channel(bouquet_t *bq, service_t *t)
381 {
382 idnode_list_mapping_t *ilm, *ilm_next;
383
384 ilm = LIST_FIRST(&t->s_channels);
385 while (ilm) {
386 ilm_next = LIST_NEXT(ilm, ilm_in1_link);
387 if (((channel_t *)ilm->ilm_in2)->ch_bouquet == bq) {
388 tvhinfo(LS_BOUQUET, "%s / %s: unmapped from %s",
389 channel_get_name((channel_t *)ilm->ilm_in2), t->s_nicename,
390 bq->bq_name ?: "<unknown>");
391 channel_delete((channel_t *)ilm->ilm_in2, 1);
392 }
393 ilm = ilm_next;
394 }
395 }
396
397 /**
398 *
399 */
400 void
bouquet_notify_service_enabled(service_t * t)401 bouquet_notify_service_enabled(service_t *t)
402 {
403 bouquet_t *bq;
404
405 lock_assert(&global_lock);
406
407 RB_FOREACH(bq, &bouquets, bq_link)
408 if (idnode_set_exists(bq->bq_services, &t->s_id)) {
409 if (!t->s_enabled)
410 bouquet_unmap_channel(bq, t);
411 else if (bq->bq_enabled && bq->bq_maptoch)
412 bouquet_map_channel(bq, t);
413 }
414 }
415
416 /*
417 *
418 */
419 static void
bouquet_remove_service(bouquet_t * bq,service_t * s,int delconf)420 bouquet_remove_service(bouquet_t *bq, service_t *s, int delconf)
421 {
422 tvhtrace(LS_BOUQUET, "remove service %s from %s",
423 s->s_nicename, bq->bq_name ?: "<unknown>");
424 idnode_set_remove(bq->bq_services, &s->s_id);
425 if (delconf)
426 bouquet_unmap_channel(bq, s);
427 }
428
429 /*
430 *
431 */
432 void
bouquet_completed(bouquet_t * bq,uint32_t seen)433 bouquet_completed(bouquet_t *bq, uint32_t seen)
434 {
435 idnode_set_t *remove;
436 service_t *s;
437 service_lcn_t *lcn, *lcn_next;
438 size_t z;
439
440 if (!bq)
441 return;
442
443 if (seen != bq->bq_services_seen) {
444 bq->bq_services_seen = seen;
445 bq->bq_saveflag = 1;
446 }
447
448 tvhtrace(LS_BOUQUET, "%s: completed: enabled=%d active=%zi old=%zi seen=%u",
449 bq->bq_name ?: "", bq->bq_enabled, bq->bq_active_services->is_count,
450 bq->bq_services->is_count, seen);
451
452 if (!bq->bq_enabled)
453 goto save;
454
455 /* Add/Remove services */
456 remove = idnode_set_create(0);
457 for (z = 0; z < bq->bq_services->is_count; z++)
458 if (!idnode_set_exists(bq->bq_active_services, bq->bq_services->is_array[z]))
459 idnode_set_add(remove, bq->bq_services->is_array[z], NULL, NULL);
460 for (z = 0; z < remove->is_count; z++)
461 bouquet_remove_service(bq, (service_t *)remove->is_array[z], 1);
462 idnode_set_free(remove);
463
464 /* Remove no longer used LCNs */
465 for (z = 0; z < bq->bq_services->is_count; z++) {
466 s = (service_t *)bq->bq_services->is_array[z];
467 for (lcn = LIST_FIRST(&s->s_lcns); lcn; lcn = lcn_next) {
468 lcn_next = LIST_NEXT(lcn, sl_link);
469 if (lcn->sl_bouquet != bq) continue;
470 if (!lcn->sl_seen) {
471 LIST_REMOVE(lcn, sl_link);
472 free(lcn);
473 } else {
474 lcn->sl_seen = 0;
475 }
476 }
477 }
478
479
480 idnode_set_free(bq->bq_active_services);
481 bq->bq_active_services = idnode_set_create(1);
482
483 save:
484 if (bq->bq_saveflag) {
485 bq->bq_saveflag = 0;
486 idnode_changed(&bq->bq_id);
487 }
488 }
489
490 /*
491 *
492 */
493 void
bouquet_map_to_channels(bouquet_t * bq)494 bouquet_map_to_channels(bouquet_t *bq)
495 {
496 service_t *t;
497 size_t z;
498
499 for (z = 0; z < bq->bq_services->is_count; z++) {
500 t = (service_t *)bq->bq_services->is_array[z];
501 if (bq->bq_enabled && bq->bq_maptoch) {
502 bouquet_map_channel(bq, t);
503 } else {
504 bouquet_unmap_channel(bq, t);
505 }
506 }
507
508 if (!bq->bq_enabled) {
509 if (bq->bq_services->is_count) {
510 idnode_set_free(bq->bq_services);
511 bq->bq_services = idnode_set_create(1);
512 bq->bq_saveflag = 1;
513 }
514 if (bq->bq_active_services->is_count) {
515 idnode_set_free(bq->bq_active_services);
516 bq->bq_active_services = idnode_set_create(1);
517 }
518 }
519 }
520
521 /*
522 *
523 */
524 void
bouquet_notify_channels(bouquet_t * bq)525 bouquet_notify_channels(bouquet_t *bq)
526 {
527 idnode_list_mapping_t *ilm;
528 service_t *t;
529 size_t z;
530
531 for (z = 0; z < bq->bq_services->is_count; z++) {
532 t = (service_t *)bq->bq_services->is_array[z];
533 LIST_FOREACH(ilm, &t->s_channels, ilm_in1_link)
534 if (((channel_t *)ilm->ilm_in2)->ch_bouquet == bq)
535 idnode_notify_changed(ilm->ilm_in2);
536 }
537 }
538
539 /*
540 *
541 */
542 uint64_t
bouquet_get_channel_number(bouquet_t * bq,service_t * t)543 bouquet_get_channel_number(bouquet_t *bq, service_t *t)
544 {
545 service_lcn_t *tl;
546
547 LIST_FOREACH(tl, &t->s_lcns, sl_link)
548 if (tl->sl_bouquet == bq)
549 return (int64_t)tl->sl_lcn;
550 return 0;
551 }
552
553 /*
554 *
555 */
556 static const char *
bouquet_get_tag_name(bouquet_t * bq,service_t * t)557 bouquet_get_tag_name(bouquet_t *bq, service_t *t)
558 {
559 return 0;
560 }
561
562 /**
563 *
564 */
565 void
bouquet_delete(bouquet_t * bq)566 bouquet_delete(bouquet_t *bq)
567 {
568 char ubuf[UUID_HEX_SIZE];
569 if (bq == NULL) return;
570 bq->bq_enabled = 0;
571 bouquet_map_to_channels(bq);
572 if (!bq->bq_shield) {
573 hts_settings_remove("bouquet/%s", idnode_uuid_as_str(&bq->bq_id, ubuf));
574 bouquet_destroy(bq);
575 } else {
576 idnode_set_free(bq->bq_services);
577 bq->bq_services = idnode_set_create(1);
578 idnode_changed(&bq->bq_id);
579 }
580 }
581
582 /**
583 *
584 */
585 void
bouquet_change_comment(bouquet_t * bq,const char * comment,int replace)586 bouquet_change_comment ( bouquet_t *bq, const char *comment, int replace )
587 {
588 if (!replace && bq->bq_comment && bq->bq_comment[0])
589 return;
590 free(bq->bq_comment);
591 bq->bq_comment = comment ? strdup(comment) : NULL;
592 bq->bq_saveflag = 1;
593 }
594
595 /*
596 *
597 */
598 void
bouquet_scan(bouquet_t * bq)599 bouquet_scan ( bouquet_t *bq )
600 {
601 void mpegts_mux_bouquet_rescan ( const char *src, const char *extra );
602 void iptv_bouquet_trigger_by_uuid( const char *uuid );
603 #if ENABLE_IPTV
604 if (bq->bq_src && strncmp(bq->bq_src, "iptv-network://", 15) == 0)
605 return iptv_bouquet_trigger_by_uuid(bq->bq_src + 15);
606 #endif
607 if (bq->bq_src && strncmp(bq->bq_src, "exturl://", 9) == 0)
608 return bouquet_download_trigger(bq);
609 mpegts_mux_bouquet_rescan(bq->bq_src, bq->bq_comment);
610 bq->bq_rescan = 0;
611 }
612
613 /*
614 *
615 */
616 void
bouquet_detach(channel_t * ch)617 bouquet_detach ( channel_t *ch )
618 {
619 bouquet_t *bq = ch->ch_bouquet;
620 idnode_list_mapping_t *ilm;
621 service_lcn_t *tl;
622 service_t *t;
623 int64_t n = 0;
624
625 if (!bq)
626 return;
627 LIST_FOREACH(ilm, &ch->ch_services, ilm_in2_link) {
628 t = (service_t *)ilm->ilm_in1;
629 LIST_FOREACH(tl, &t->s_lcns, sl_link)
630 if (tl->sl_bouquet == bq) {
631 n = (int64_t)tl->sl_lcn;
632 goto found;
633 }
634 }
635 found:
636 if (n) {
637 n += (int64_t)ch->ch_bouquet->bq_lcn_offset * CHANNEL_SPLIT;
638 ch->ch_number = n;
639 }
640 ch->ch_bouquet = NULL;
641 idnode_changed(&ch->ch_id);
642 }
643
644 /* **************************************************************************
645 * Class definition
646 * **************************************************************************/
647
648 static htsmsg_t *
bouquet_class_save(idnode_t * self,char * filename,size_t fsize)649 bouquet_class_save(idnode_t *self, char *filename, size_t fsize)
650 {
651 bouquet_t *bq = (bouquet_t *)self;
652 htsmsg_t *c = htsmsg_create_map();
653 char ubuf[UUID_HEX_SIZE];
654 idnode_save(&bq->bq_id, c);
655 snprintf(filename, fsize, "bouquet/%s", idnode_uuid_as_str(&bq->bq_id, ubuf));
656 if (bq->bq_shield)
657 htsmsg_add_bool(c, "shield", 1);
658 bq->bq_saveflag = 0;
659 return c;
660 }
661
662 static void
bouquet_class_delete(idnode_t * self)663 bouquet_class_delete(idnode_t *self)
664 {
665 bouquet_delete((bouquet_t *)self);
666 }
667
668 static const char *
bouquet_class_get_title(idnode_t * self,const char * lang)669 bouquet_class_get_title (idnode_t *self, const char *lang)
670 {
671 bouquet_t *bq = (bouquet_t *)self;
672
673 if (bq->bq_comment && bq->bq_comment[0] != '\0')
674 return bq->bq_comment;
675 return bq->bq_name ?: "";
676 }
677
678 /* exported for others */
679 htsmsg_t *
bouquet_class_get_list(void * o,const char * lang)680 bouquet_class_get_list(void *o, const char *lang)
681 {
682 htsmsg_t *m = htsmsg_create_map();
683 htsmsg_add_str(m, "type", "api");
684 htsmsg_add_str(m, "uri", "bouquet/list");
685 htsmsg_add_str(m, "event", "bouquet");
686 return m;
687 }
688
689 static void
bouquet_class_enabled_notify(void * obj,const char * lang)690 bouquet_class_enabled_notify ( void *obj, const char *lang )
691 {
692 bouquet_t *bq = obj;
693
694 if (bq->bq_enabled)
695 bouquet_scan(bq);
696 bouquet_map_to_channels(bq);
697 }
698
699 static void
bouquet_class_maptoch_notify(void * obj,const char * lang)700 bouquet_class_maptoch_notify ( void *obj, const char *lang )
701 {
702 bouquet_map_to_channels((bouquet_t *)obj);
703 }
704
705 static void
bouquet_class_lcn_offset_notify(void * obj,const char * lang)706 bouquet_class_lcn_offset_notify ( void *obj, const char *lang )
707 {
708 if (((bouquet_t *)obj)->bq_in_load)
709 return;
710 bouquet_notify_channels((bouquet_t *)obj);
711 }
712
713 static idnode_slist_t bouquest_class_mapopt_slist[] = {
714 {
715 .id = "mapnolcn",
716 .name = N_("Map zero-numbered channels"),
717 .off = offsetof(bouquet_t, bq_mapnolcn),
718 },
719 {
720 .id = "mapnoname",
721 .name = N_("Map unnamed channels"),
722 .off = offsetof(bouquet_t, bq_mapnoname),
723 },
724 {
725 .id = "mapradio",
726 .name = N_("Map radio channels"),
727 .off = offsetof(bouquet_t, bq_mapradio),
728 },
729 {
730 .id = "encrypted",
731 .name = N_("Map encrypted services"),
732 .off = offsetof(bouquet_t, bq_mapencrypted),
733 },
734 {
735 .id = "merge_name",
736 .name = N_("Merge same name"),
737 .off = offsetof(bouquet_t, bq_mapmergename),
738 },
739 {}
740 };
741
742 static htsmsg_t *
bouquet_class_mapopt_enum(void * obj,const char * lang)743 bouquet_class_mapopt_enum ( void *obj, const char *lang )
744 {
745 return idnode_slist_enum(obj, bouquest_class_mapopt_slist, lang);
746 }
747
748 static const void *
bouquet_class_mapopt_get(void * obj)749 bouquet_class_mapopt_get ( void *obj )
750 {
751 return idnode_slist_get(obj, bouquest_class_mapopt_slist);
752 }
753
754 static char *
bouquet_class_mapopt_rend(void * obj,const char * lang)755 bouquet_class_mapopt_rend ( void *obj, const char *lang )
756 {
757 return idnode_slist_rend(obj, bouquest_class_mapopt_slist, lang);
758 }
759
760 static int
bouquet_class_mapopt_set(void * obj,const void * p)761 bouquet_class_mapopt_set ( void *obj, const void *p )
762 {
763 return idnode_slist_set(obj, bouquest_class_mapopt_slist, p);
764 }
765
766 static void
bouquet_class_mapopt_notify(void * obj,const char * lang)767 bouquet_class_mapopt_notify ( void *obj, const char *lang )
768 {
769 bouquet_t *bq = obj;
770 service_t *t;
771 size_t z;
772
773 if (bq->bq_in_load)
774 return;
775 if (bq->bq_enabled && bq->bq_maptoch) {
776 for (z = 0; z < bq->bq_services->is_count; z++) {
777 t = (service_t *)bq->bq_services->is_array[z];
778 if (!bq->bq_mapradio && service_is_radio(t))
779 bouquet_unmap_channel(bq, t);
780 else if (!bq->bq_mapnoname && noname(service_get_channel_name(t)))
781 bouquet_unmap_channel(bq, t);
782 else if (!bq->bq_mapradio && service_is_radio(t))
783 bouquet_unmap_channel(bq, t);
784 else if (!bq->bq_mapencrypted && service_is_encrypted(t))
785 bouquet_unmap_channel(bq, t);
786 }
787 }
788 bouquet_map_to_channels(bq);
789 }
790
791 static idnode_slist_t bouquest_class_chtag_slist[] = {
792 {
793 .id = "bouquet_tag",
794 .name = N_("Create bouquet tag"),
795 .off = offsetof(bouquet_t, bq_chtag),
796 },
797 {
798 .id = "type_tags",
799 .name = N_("Create type-based tags"),
800 .off = offsetof(bouquet_t, bq_chtag_type_tags),
801 },
802 {
803 .id = "providers_tags",
804 .name = N_("Create provider name tags"),
805 .off = offsetof(bouquet_t, bq_chtag_provider_tags),
806 },
807 {
808 .id = "network_tags",
809 .name = N_("Create network name tags"),
810 .off = offsetof(bouquet_t, bq_chtag_network_tags),
811 },
812 {}
813 };
814
815 static htsmsg_t *
bouquet_class_chtag_enum(void * obj,const char * lang)816 bouquet_class_chtag_enum ( void *obj, const char *lang )
817 {
818 return idnode_slist_enum(obj, bouquest_class_chtag_slist, lang);
819 }
820
821 static const void *
bouquet_class_chtag_get(void * obj)822 bouquet_class_chtag_get ( void *obj )
823 {
824 return idnode_slist_get(obj, bouquest_class_chtag_slist);
825 }
826
827 static char *
bouquet_class_chtag_rend(void * obj,const char * lang)828 bouquet_class_chtag_rend ( void *obj, const char *lang )
829 {
830 return idnode_slist_rend(obj, bouquest_class_chtag_slist, lang);
831 }
832
833 static int
bouquet_class_chtag_set(void * obj,const void * p)834 bouquet_class_chtag_set ( void *obj, const void *p )
835 {
836 return idnode_slist_set(obj, bouquest_class_chtag_slist, p);
837 }
838
839 static void
bouquet_class_chtag_notify(void * obj,const char * lang)840 bouquet_class_chtag_notify ( void *obj, const char *lang )
841 {
842 bouquet_t *bq = obj;
843 service_t *t;
844 idnode_list_mapping_t *ilm;
845 channel_tag_t *ct;
846 size_t z;
847
848 if (bq->bq_in_load)
849 return;
850 if (!bq->bq_chtag && bq->bq_enabled && bq->bq_maptoch) {
851 ct = bouquet_tag(bq, 0);
852 if (!ct)
853 return;
854 for (z = 0; z < bq->bq_services->is_count; z++) {
855 t = (service_t *)bq->bq_services->is_array[z];
856 LIST_FOREACH(ilm, &t->s_channels, ilm_in1_link)
857 if (((channel_t *)ilm->ilm_in2)->ch_bouquet == bq)
858 break;
859 if (ilm)
860 channel_tag_unmap((channel_t *)ilm->ilm_in2, ct);
861 }
862 }
863 bouquet_map_to_channels(bq);
864 }
865
866 static const void *
bouquet_class_chtag_ref_get(void * obj)867 bouquet_class_chtag_ref_get ( void *obj )
868 {
869 bouquet_t *bq = obj;
870
871 if (bq->bq_chtag_ptr)
872 idnode_uuid_as_str(&bq->bq_chtag_ptr->ct_id, prop_sbuf);
873 else
874 prop_sbuf[0] = '\0';
875 return &prop_sbuf_ptr;
876 }
877
878 static char *
bouquet_class_chtag_ref_rend(void * obj,const char * lang)879 bouquet_class_chtag_ref_rend ( void *obj, const char *lang )
880 {
881 bouquet_t *bq = obj;
882 if (bq->bq_chtag_ptr)
883 return strdup(bq->bq_chtag_ptr->ct_name ?: "");
884 else
885 return strdup("");
886 }
887
888 static int
bouquet_class_chtag_ref_set(void * obj,const void * p)889 bouquet_class_chtag_ref_set ( void *obj, const void *p )
890 {
891 bouquet_t *bq = obj;
892
893 free((char *)bq->bq_chtag_waiting);
894 bq->bq_chtag_waiting = NULL;
895 if (bq->bq_in_load)
896 bq->bq_chtag_waiting = strdup((const char *)p);
897 return 0;
898 }
899
900 static const void *
bouquet_class_services_get(void * obj)901 bouquet_class_services_get ( void *obj )
902 {
903 htsmsg_t *m = htsmsg_create_map(), *e;
904 bouquet_t *bq = obj;
905 service_t *t;
906 int64_t lcn;
907 const char *tag;
908 size_t z;
909 char ubuf[UUID_HEX_SIZE];
910
911 /* Add all */
912 for (z = 0; z < bq->bq_services->is_count; z++) {
913 t = (service_t *)bq->bq_services->is_array[z];
914 e = htsmsg_create_map();
915 if ((lcn = bouquet_get_channel_number(bq, t)) != 0)
916 htsmsg_add_s64(e, "lcn", lcn);
917 if ((tag = bouquet_get_tag_name(bq, t)) != NULL)
918 htsmsg_add_str(e, "tag", tag);
919 htsmsg_add_msg(m, idnode_uuid_as_str(&t->s_id, ubuf), e);
920 }
921
922 return m;
923 }
924
925 static char *
bouquet_class_services_rend(void * obj,const char * lang)926 bouquet_class_services_rend ( void *obj, const char *lang )
927 {
928 bouquet_t *bq = obj;
929 const char *sc = N_("Service count %zi");
930 char buf[32];
931 snprintf(buf, sizeof(buf), tvh_gettext_lang(lang, sc), bq->bq_services->is_count);
932 return strdup(buf);
933 }
934
935 static int
bouquet_class_services_set(void * obj,const void * p)936 bouquet_class_services_set ( void *obj, const void *p )
937 {
938 bouquet_t *bq = obj;
939
940 if (bq->bq_services_waiting)
941 htsmsg_destroy(bq->bq_services_waiting);
942 bq->bq_services_waiting = NULL;
943 if (bq->bq_in_load)
944 bq->bq_services_waiting = htsmsg_copy((htsmsg_t *)p);
945 return 0;
946 }
947
948 static const void *
bouquet_class_services_count_get(void * obj)949 bouquet_class_services_count_get ( void *obj )
950 {
951 static uint32_t u32;
952 bouquet_t *bq = obj;
953
954 u32 = bq->bq_services->is_count;
955 return &u32;
956 }
957
958 static void
bouquet_class_ext_url_notify(void * obj,const char * lang)959 bouquet_class_ext_url_notify ( void *obj, const char *lang )
960 {
961 bouquet_download_trigger((bouquet_t *)obj);
962 }
963
964 CLASS_DOC(bouquet)
965 PROP_DOC(bouquet_mapping_options)
966 PROP_DOC(bouquet_tagging)
967
968 const idclass_t bouquet_class = {
969 .ic_class = "bouquet",
970 .ic_caption = N_("Bouquets"),
971 .ic_doc = tvh_doc_bouquet_class,
972 .ic_event = "bouquet",
973 .ic_perm_def = ACCESS_ADMIN,
974 .ic_save = bouquet_class_save,
975 .ic_get_title = bouquet_class_get_title,
976 .ic_delete = bouquet_class_delete,
977 .ic_properties = (const property_t[]){
978 {
979 .type = PT_BOOL,
980 .id = "enabled",
981 .name = N_("Enabled"),
982 .desc = N_("Enable/disable the bouquet."),
983 .def.i = 1,
984 .off = offsetof(bouquet_t, bq_enabled),
985 .notify = bouquet_class_enabled_notify,
986 },
987 {
988 .type = PT_BOOL,
989 .id = "maptoch",
990 .name = N_("Auto-Map to channels"),
991 .desc = N_("Automatically map channels defined within the "
992 "bouquet."),
993 .off = offsetof(bouquet_t, bq_maptoch),
994 .notify = bouquet_class_maptoch_notify,
995 },
996 {
997 .type = PT_INT,
998 .islist = 1,
999 .id = "mapopt",
1000 .name = N_("Channel mapping options"),
1001 .desc = N_("Options to use/used when mapping - see Help for details."),
1002 .doc = prop_doc_bouquet_mapping_options,
1003 .notify = bouquet_class_mapopt_notify,
1004 .list = bouquet_class_mapopt_enum,
1005 .get = bouquet_class_mapopt_get,
1006 .set = bouquet_class_mapopt_set,
1007 .rend = bouquet_class_mapopt_rend,
1008 .opts = PO_ADVANCED | PO_DOC_NLIST,
1009 },
1010 {
1011 .type = PT_INT,
1012 .islist = 1,
1013 .id = "chtag",
1014 .name = N_("Create tags"),
1015 .desc = N_("Create and link these tags to channels when "
1016 "mapping."),
1017 .doc = prop_doc_bouquet_tagging,
1018 .notify = bouquet_class_chtag_notify,
1019 .list = bouquet_class_chtag_enum,
1020 .get = bouquet_class_chtag_get,
1021 .set = bouquet_class_chtag_set,
1022 .rend = bouquet_class_chtag_rend,
1023 .opts = PO_ADVANCED | PO_DOC_NLIST,
1024 },
1025 {
1026 .type = PT_STR,
1027 .id = "chtag_ref",
1028 .name = N_("Channel tag reference"),
1029 .get = bouquet_class_chtag_ref_get,
1030 .set = bouquet_class_chtag_ref_set,
1031 .rend = bouquet_class_chtag_ref_rend,
1032 .opts = PO_RDONLY | PO_HIDDEN | PO_NOUI,
1033 },
1034 {
1035 .type = PT_STR,
1036 .id = "name",
1037 .name = N_("Name"),
1038 .desc = N_("Name of the bouquet."),
1039 .off = offsetof(bouquet_t, bq_name),
1040 },
1041 {
1042 .type = PT_STR,
1043 .id = "ext_url",
1044 .name = N_("External URL"),
1045 .desc = N_("External URL of the bouquet."),
1046 .off = offsetof(bouquet_t, bq_ext_url),
1047 .opts = PO_HIDDEN | PO_MULTILINE,
1048 .notify = bouquet_class_ext_url_notify,
1049 },
1050 {
1051 .type = PT_BOOL,
1052 .id = "ssl_peer_verify",
1053 .name = N_("SSL verify peer"),
1054 .desc = N_("Verify the SSL certificate."),
1055 .off = offsetof(bouquet_t, bq_ssl_peer_verify),
1056 .opts = PO_ADVANCED | PO_HIDDEN | PO_EXPERT,
1057 .notify = bouquet_class_ext_url_notify,
1058 },
1059 {
1060 .type = PT_U32,
1061 .id = "ext_url_period",
1062 .name = N_("Re-fetch period (mins)"),
1063 .desc = N_("Re-fetch the bouquet every x minutes."),
1064 .off = offsetof(bouquet_t, bq_ext_url_period),
1065 .opts = PO_ADVANCED | PO_HIDDEN,
1066 .notify = bouquet_class_ext_url_notify,
1067 .def.i = 60
1068 },
1069 {
1070 .type = PT_STR,
1071 .id = "source",
1072 .name = N_("Source"),
1073 .desc = N_("Bouquet source."),
1074 .off = offsetof(bouquet_t, bq_src),
1075 .opts = PO_RDONLY | PO_ADVANCED,
1076 },
1077 {
1078 .type = PT_STR,
1079 .islist = 1,
1080 .id = "services",
1081 .name = N_("Services"),
1082 .desc = N_("Number of services."),
1083 .get = bouquet_class_services_get,
1084 .set = bouquet_class_services_set,
1085 .rend = bouquet_class_services_rend,
1086 .opts = PO_RDONLY | PO_HIDDEN,
1087 },
1088 {
1089 .type = PT_U32,
1090 .id = "services_seen",
1091 .name = N_("Services seen"),
1092 .desc = N_("Total number of services seen."),
1093 .off = offsetof(bouquet_t, bq_services_seen),
1094 .opts = PO_RDONLY,
1095 },
1096 {
1097 .type = PT_U32,
1098 .id = "services_count",
1099 .name = N_("Services"),
1100 .desc = N_("Total number of services."),
1101 .get = bouquet_class_services_count_get,
1102 .opts = PO_RDONLY | PO_NOSAVE,
1103 },
1104 {
1105 .type = PT_STR,
1106 .id = "comment",
1107 .name = N_("Comment"),
1108 .desc = N_("Free-form text field, enter whatever you like."),
1109 .off = offsetof(bouquet_t, bq_comment),
1110 },
1111 {
1112 .type = PT_U32,
1113 .id = "lcn_off",
1114 .name = N_("Channel number offset"),
1115 .desc = N_("Offset the mapped channel numbers by x "
1116 "(value here + channel number)."),
1117 .off = offsetof(bouquet_t, bq_lcn_offset),
1118 .notify = bouquet_class_lcn_offset_notify,
1119 .opts = PO_ADVANCED
1120 },
1121 {}
1122 }
1123 };
1124
1125 /**
1126 *
1127 */
1128 static void
bouquet_download_trigger0(void * aux)1129 bouquet_download_trigger0(void *aux)
1130 {
1131 bouquet_download_t *bqd = aux;
1132 bouquet_t *bq = bqd->bq;
1133
1134 download_start(&bqd->download, bq->bq_ext_url, bqd);
1135 mtimer_arm_rel(&bqd->timer, bouquet_download_trigger0, bqd,
1136 sec2mono(MAX(1, bq->bq_ext_url_period) * 60));
1137 }
1138
1139 static void
bouquet_download_trigger(bouquet_t * bq)1140 bouquet_download_trigger(bouquet_t *bq)
1141 {
1142 bouquet_download_t *bqd = bq->bq_download;
1143 if (bqd) {
1144 bqd->download.ssl_peer_verify = bq->bq_ssl_peer_verify;
1145 bouquet_download_trigger0(bqd);
1146 }
1147 }
1148
1149 static void
bouquet_download_stop(void * aux)1150 bouquet_download_stop(void *aux)
1151 {
1152 bouquet_download_t *bqd = aux;
1153 mtimer_disarm(&bqd->timer);
1154 }
1155
1156 static int
parse_enigma2(bouquet_t * bq,char * data)1157 parse_enigma2(bouquet_t *bq, char *data)
1158 {
1159 extern service_t *mpegts_service_find_e2(uint32_t stype, uint32_t sid,
1160 uint32_t tsid, uint32_t onid,
1161 uint32_t hash);
1162 char *argv[11], *p, *tagname = NULL, *name;
1163 long lv, stype, sid, tsid, onid, hash;
1164 uint32_t seen = 0;
1165 int n, ver = 2;
1166
1167 while (*data) {
1168 if (strncmp(data, "#NAME ", 6) == 0) {
1169 for (data += 6, p = data; *data && *data != '\r' && *data != '\n'; data++);
1170 if (*data) { *data = '\0'; data++; }
1171 if (bq->bq_name == NULL || bq->bq_name[0] == '\0')
1172 bq->bq_name = strdup(p);
1173 } if (strncmp(data, "#SERVICE ", 9) == 0) {
1174 data += 9, p = data;
1175 service:
1176 while (*data && *data != '\r' && *data != '\n') data++;
1177 if (*data) { *data = '\0'; data++; }
1178 n = http_tokenize(p, argv, ARRAY_SIZE(argv), ':');
1179 if (n >= 10) {
1180 if (strtol(argv[0], NULL, 0) != 1) goto next; /* item type */
1181 lv = strtol(argv[1], NULL, 16); /* service flags? */
1182 if (lv != 0 && lv != 0x64) goto next;
1183 stype = strtol(argv[2], NULL, 16); /* DVB service type */
1184 sid = strtol(argv[3], NULL, 16); /* DVB service ID */
1185 tsid = strtol(argv[4], NULL, 16);
1186 onid = strtol(argv[5], NULL, 16);
1187 hash = strtol(argv[6], NULL, 16);
1188 name = n > 10 ? argv[10] : NULL;
1189 if (lv == 0) {
1190 service_t *s = mpegts_service_find_e2(stype, sid, tsid, onid, hash);
1191 if (s)
1192 bouquet_add_service(bq, s, ((int64_t)++seen) * CHANNEL_SPLIT, tagname);
1193 } else if (lv == 0x64) {
1194 tagname = name;
1195 }
1196 }
1197 } else if (strncmp(data, "#SERVICE: ", 10) == 0) {
1198 ver = 1, data += 10, p = data;
1199 goto service;
1200 } else {
1201 while (*data && *data != '\r' && *data != '\n') data++;
1202 }
1203 next:
1204 while (*data && (*data == '\r' || *data == '\n')) data++;
1205 }
1206 bouquet_completed(bq, seen);
1207 tvhinfo(LS_BOUQUET, "parsed Enigma%d bouquet %s (%d services)", ver, bq->bq_name, seen);
1208 return 0;
1209 }
1210
1211 static int
bouquet_download_process(void * aux,const char * last_url,const char * host_url,char * data,size_t len)1212 bouquet_download_process(void *aux, const char *last_url, const char *host_url,
1213 char *data, size_t len)
1214 {
1215 bouquet_download_t *bqd = aux;
1216 while (*data) {
1217 while (*data && *data < ' ') data++;
1218 if (*data && !strncmp(data, "#NAME ", 6))
1219 return parse_enigma2(bqd->bq, data);
1220 }
1221 return 0;
1222 }
1223
1224 /**
1225 *
1226 */
1227 void
bouquet_init(void)1228 bouquet_init(void)
1229 {
1230 htsmsg_t *c, *m;
1231 htsmsg_field_t *f;
1232 bouquet_t *bq;
1233
1234 RB_INIT(&bouquets);
1235 idclass_register(&bouquet_class);
1236
1237 /* Load */
1238 if ((c = hts_settings_load("bouquet")) != NULL) {
1239 HTSMSG_FOREACH(f, c) {
1240 if (!(m = htsmsg_field_get_map(f))) continue;
1241 bq = bouquet_create(f->hmf_name, m, NULL, NULL);
1242 if (bq)
1243 bq->bq_saveflag = 0;
1244 }
1245 htsmsg_destroy(c);
1246 }
1247 }
1248
1249 void
bouquet_service_resolve(void)1250 bouquet_service_resolve(void)
1251 {
1252 bouquet_t *bq;
1253 htsmsg_t *e;
1254 htsmsg_field_t *f;
1255 service_t *s;
1256 int64_t lcn;
1257 const char *tag;
1258 int saveflag;
1259
1260 lock_assert(&global_lock);
1261
1262 RB_FOREACH(bq, &bouquets, bq_link) {
1263 if (!bq->bq_services_waiting)
1264 continue;
1265 saveflag = bq->bq_saveflag;
1266 if (bq->bq_enabled) {
1267 HTSMSG_FOREACH(f, bq->bq_services_waiting) {
1268 if ((e = htsmsg_field_get_map(f)) == NULL) continue;
1269 lcn = htsmsg_get_s64_or_default(e, "lcn", 0);
1270 tag = htsmsg_get_str(e, "tag");
1271 s = service_find_by_identifier(f->hmf_name);
1272 if (s)
1273 bouquet_add_service(bq, s, lcn, tag);
1274 }
1275 }
1276 htsmsg_destroy(bq->bq_services_waiting);
1277 bq->bq_services_waiting = NULL;
1278 bq->bq_saveflag = saveflag;
1279 }
1280 }
1281
1282 void
bouquet_done(void)1283 bouquet_done(void)
1284 {
1285 bouquet_t *bq;
1286
1287 pthread_mutex_lock(&global_lock);
1288 while ((bq = RB_FIRST(&bouquets)) != NULL)
1289 bouquet_destroy(bq);
1290 pthread_mutex_unlock(&global_lock);
1291 }
1292