1 /*
2 * EPG Grabber - module functions
3 * Copyright (C) 2012 Adam Sutton
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 <assert.h>
20 #include <string.h>
21 #include <pthread.h>
22 #include <unistd.h>
23 #include <signal.h>
24 #include <sys/types.h>
25 #include <sys/socket.h>
26 #include <sys/un.h>
27
28 #include "tvheadend.h"
29 #include "settings.h"
30 #include "htsmsg.h"
31 #include "htsmsg_xml.h"
32 #include "service.h"
33 #include "channels.h"
34 #include "spawn.h"
35 #include "file.h"
36 #include "epg.h"
37 #include "epggrab.h"
38 #include "epggrab/private.h"
39 extern gtimer_t epggrab_save_timer;
40
41 /* **************************************************************************
42 * Module Access
43 * *************************************************************************/
44
epggrab_module_find_by_id(const char * id)45 epggrab_module_t* epggrab_module_find_by_id ( const char *id )
46 {
47 epggrab_module_t *m;
48 LIST_FOREACH(m, &epggrab_modules, link)
49 if (!strcmp(m->id, id))
50 return m;
51 return NULL;
52 }
53
54 const char *
epggrab_module_type(epggrab_module_t * mod)55 epggrab_module_type(epggrab_module_t *mod)
56 {
57 switch (mod->type) {
58 case EPGGRAB_OTA: return N_("Over-the-air");
59 case EPGGRAB_INT: return N_("Internal");
60 case EPGGRAB_EXT: return N_("External");
61 default: return N_("Unknown");
62 }
63 }
64
65 const char *
epggrab_module_get_status(epggrab_module_t * mod)66 epggrab_module_get_status(epggrab_module_t *mod)
67 {
68 if (mod->enabled)
69 return "epggrabmodEnabled";
70 return "epggrabmodNone";
71 }
72
73 /*
74 * Class
75 */
76
epggrab_mod_class_title(idnode_t * self,const char * lang)77 static const char *epggrab_mod_class_title(idnode_t *self, const char *lang)
78 {
79 epggrab_module_t *mod = (epggrab_module_t *)self;
80 const char *s1 = tvh_gettext_lang(lang, epggrab_module_type(mod));
81 const char *s2 = tvh_str_default(mod->name, mod->id);
82 snprintf(prop_sbuf, PROP_SBUF_LEN, "%s: %s", s1, s2);
83 return prop_sbuf;
84 }
85
86 static void
epggrab_mod_class_changed(idnode_t * self)87 epggrab_mod_class_changed(idnode_t *self)
88 {
89 epggrab_module_t *mod = (epggrab_module_t *)self;
90 epggrab_activate_module(mod, mod->enabled);
91 idnode_changed(&epggrab_conf.idnode);
92 }
93
epggrab_mod_class_type_get(void * o)94 static const void *epggrab_mod_class_type_get(void *o)
95 {
96 static const char *s;
97 s = epggrab_module_type((epggrab_module_t *)o);
98 return &s;
99 }
100
epggrab_mod_class_type_set(void * o,const void * v)101 static int epggrab_mod_class_type_set(void *o, const void *v)
102 {
103 return 0;
104 }
105
106 CLASS_DOC(epggrabber_modules)
107 PROP_DOC(epggrabber_priority)
108
109 const idclass_t epggrab_mod_class = {
110 .ic_class = "epggrab_mod",
111 .ic_caption = N_("EPG Grabber"),
112 .ic_doc = tvh_doc_epggrabber_modules_class,
113 .ic_event = "epggrab_mod",
114 .ic_perm_def = ACCESS_ADMIN,
115 .ic_get_title = epggrab_mod_class_title,
116 .ic_changed = epggrab_mod_class_changed,
117 .ic_groups = (const property_group_t[]) {
118 {
119 .name = N_("Settings"),
120 .number = 1,
121 },
122 {}
123 },
124 .ic_properties = (const property_t[]){
125 {
126 .type = PT_STR,
127 .id = "name",
128 .name = N_("Name"),
129 .desc = N_("The EPG grabber name."),
130 .off = offsetof(epggrab_module_t, name),
131 .opts = PO_RDONLY,
132 .group = 1,
133 },
134 {
135 .type = PT_STR,
136 .id = "type",
137 .name = N_("Type"),
138 .desc = N_("The EPG grabber type."),
139 .get = epggrab_mod_class_type_get,
140 .set = epggrab_mod_class_type_set,
141 .opts = PO_RDONLY | PO_LOCALE,
142 .group = 1,
143 },
144 {
145 .type = PT_BOOL,
146 .id = "enabled",
147 .name = N_("Enabled"),
148 .desc = N_("Enable/disable the grabber."),
149 .off = offsetof(epggrab_module_t, enabled),
150 .group = 1,
151 },
152 {
153 .type = PT_INT,
154 .id = "priority",
155 .name = N_("Priority"),
156 .desc = N_("Grabber priority. This option lets you pick which "
157 "EPG grabber's data get used first. Priority is "
158 "given to the grabber with the highest value set here. "
159 "See Help for more info."),
160 .doc = prop_doc_epggrabber_priority,
161 .off = offsetof(epggrab_module_t, priority),
162 .opts = PO_EXPERT,
163 .group = 1
164 },
165 {}
166 }
167 };
168
169 const idclass_t epggrab_mod_int_class = {
170 .ic_super = &epggrab_mod_class,
171 .ic_class = "epggrab_mod_int",
172 .ic_caption = N_("Internal EPG grabber"),
173 .ic_properties = (const property_t[]){
174 {
175 .type = PT_STR,
176 .id = "path",
177 .name = N_("Path"),
178 .desc = N_("Path to the grabber executable."),
179 .off = offsetof(epggrab_module_int_t, path),
180 .opts = PO_RDONLY | PO_NOSAVE,
181 .group = 1
182 },
183 {
184 .type = PT_STR,
185 .id = "args",
186 .name = N_("Extra arguments"),
187 .desc = N_("Additional arguments to pass to the grabber."),
188 .off = offsetof(epggrab_module_int_t, args),
189 .opts = PO_ADVANCED,
190 .group = 1
191 },
192 {}
193 }
194 };
195
196 const idclass_t epggrab_mod_ext_class = {
197 .ic_super = &epggrab_mod_class,
198 .ic_class = "epggrab_mod_ext",
199 .ic_caption = N_("External EPG grabber"),
200 .ic_properties = (const property_t[]){
201 {
202 .type = PT_STR,
203 .id = "path",
204 .name = N_("Path"),
205 .desc = N_("Path to the socket Tvheadend will read data from."),
206 .off = offsetof(epggrab_module_ext_t, path),
207 .opts = PO_RDONLY | PO_NOSAVE,
208 .group = 1
209 },
210 {}
211 }
212 };
213
214 const idclass_t epggrab_mod_ota_class = {
215 .ic_super = &epggrab_mod_class,
216 .ic_class = "epggrab_mod_ota",
217 .ic_caption = N_("Over-the-air EPG grabber"),
218 .ic_properties = (const property_t[]){
219 {}
220 }
221 };
222
223 /* **************************************************************************
224 * Generic module routines
225 * *************************************************************************/
226
epggrab_module_create(epggrab_module_t * skel,const idclass_t * cls,const char * id,int subsys,const char * saveid,const char * name,int priority)227 epggrab_module_t *epggrab_module_create
228 ( epggrab_module_t *skel, const idclass_t *cls,
229 const char *id, int subsys, const char *saveid,
230 const char *name, int priority )
231 {
232 assert(skel);
233
234 /* Setup */
235 skel->id = strdup(id);
236 skel->subsys = subsys;
237 skel->saveid = strdup(saveid ?: id);
238 skel->name = strdup(name);
239 skel->priority = priority;
240 RB_INIT(&skel->channels);
241
242 /* Insert */
243 assert(!epggrab_module_find_by_id(id));
244 LIST_INSERT_HEAD(&epggrab_modules, skel, link);
245 tvhinfo(LS_EPGGRAB, "module %s created", id);
246
247 idnode_insert(&skel->idnode, NULL, cls, 0);
248
249 return skel;
250 }
251
252 /*
253 * Run the parse
254 */
epggrab_module_parse(void * m,htsmsg_t * data)255 void epggrab_module_parse( void *m, htsmsg_t *data )
256 {
257 int64_t tm1, tm2;
258 int save = 0;
259 epggrab_stats_t stats;
260 epggrab_module_int_t *mod = m;
261
262 /* Parse */
263 memset(&stats, 0, sizeof(stats));
264 tm1 = getfastmonoclock();
265 save |= mod->parse(mod, data, &stats);
266 tm2 = getfastmonoclock();
267 htsmsg_destroy(data);
268
269 /* Debug stats */
270 tvhinfo(mod->subsys, "%s: parse took %"PRId64" seconds", mod->id, mono2sec(tm2 - tm1));
271 tvhinfo(mod->subsys, "%s: channels tot=%5d new=%5d mod=%5d",
272 mod->id, stats.channels.total, stats.channels.created,
273 stats.channels.modified);
274 tvhinfo(mod->subsys, "%s: brands tot=%5d new=%5d mod=%5d",
275 mod->id, stats.brands.total, stats.brands.created,
276 stats.brands.modified);
277 tvhinfo(mod->subsys, "%s: seasons tot=%5d new=%5d mod=%5d",
278 mod->id, stats.seasons.total, stats.seasons.created,
279 stats.seasons.modified);
280 tvhinfo(mod->subsys, "%s: episodes tot=%5d new=%5d mod=%5d",
281 mod->id, stats.episodes.total, stats.episodes.created,
282 stats.episodes.modified);
283 tvhinfo(mod->subsys, "%s: broadcasts tot=%5d new=%5d mod=%5d",
284 mod->id, stats.broadcasts.total, stats.broadcasts.created,
285 stats.broadcasts.modified);
286
287 /* Now we've parsed, do we need to save? */
288 if (save && epggrab_conf.epgdb_saveafterimport) {
289 tvhinfo(mod->subsys, "%s: scheduling save epg timer", mod->id);
290 pthread_mutex_lock(&global_lock);
291 /* Disarm any existing timer first (from a periodic save). */
292 gtimer_disarm(&epggrab_save_timer);
293 /* Reschedule for a few minutes away so if the user is
294 * refreshing from multiple xmltv sources we will give time for
295 * them to all complete before persisting, rather than persisting
296 * immediately after a parse.
297 *
298 * If periodic saving is enabled then the callback will then
299 * rearm the timer for x hours after the previous save.
300 */
301 gtimer_arm_rel(&epggrab_save_timer, epg_save_callback, NULL,
302 60 * 2);
303 pthread_mutex_unlock(&global_lock);
304 }
305 }
306
307 /* **************************************************************************
308 * Module channel routines
309 * *************************************************************************/
310
epggrab_module_channels_load(const char * modid)311 void epggrab_module_channels_load ( const char *modid )
312 {
313 epggrab_module_t *mod = NULL;
314 htsmsg_t *m, *e;
315 htsmsg_field_t *f;
316 const char *id;
317 if (!modid) return;
318 if ((m = hts_settings_load_r(1, "epggrab/%s/channels", modid))) {
319 HTSMSG_FOREACH(f, m) {
320 if ((e = htsmsg_get_map_by_field(f))) {
321 id = htsmsg_get_str(e, "modid") ?: modid;
322 if (mod == NULL || strcmp(mod->id, id))
323 mod = epggrab_module_find_by_id(id);
324 if (mod)
325 epggrab_channel_create(mod, e, f->hmf_name);
326 }
327 }
328 htsmsg_destroy(m);
329 }
330 }
331
332 /* **************************************************************************
333 * Internal module routines
334 * *************************************************************************/
335
336 static void
epggrab_module_int_done(void * m)337 epggrab_module_int_done( void *m )
338 {
339 epggrab_module_int_t *mod = m;
340 mod->active = 0;
341 free((char *)mod->path);
342 mod->path = NULL;
343 free((char *)mod->args);
344 mod->args = NULL;
345 }
346
epggrab_module_int_create(epggrab_module_int_t * skel,const idclass_t * cls,const char * id,int subsys,const char * saveid,const char * name,int priority,const char * path,char * (* grab)(void * m),int (* parse)(void * m,htsmsg_t * data,epggrab_stats_t * sta),htsmsg_t * (* trans)(void * mod,char * data))347 epggrab_module_int_t *epggrab_module_int_create
348 ( epggrab_module_int_t *skel, const idclass_t *cls,
349 const char *id, int subsys, const char *saveid,
350 const char *name, int priority,
351 const char *path,
352 char* (*grab) (void*m),
353 int (*parse) (void *m, htsmsg_t *data, epggrab_stats_t *sta),
354 htsmsg_t* (*trans) (void *mod, char *data) )
355 {
356 /* Allocate data */
357 if (!skel) skel = calloc(1, sizeof(epggrab_module_int_t));
358
359 /* Pass through */
360 epggrab_module_create((epggrab_module_t*)skel,
361 cls ?: &epggrab_mod_int_class,
362 id, subsys, saveid, name, priority);
363
364 /* Int data */
365 skel->type = EPGGRAB_INT;
366 skel->path = strdup(path);
367 skel->args = NULL;
368 skel->grab = grab ?: epggrab_module_grab_spawn;
369 skel->trans = trans ?: epggrab_module_trans_xml;
370 skel->parse = parse;
371 skel->done = epggrab_module_int_done;
372
373 return skel;
374 }
375
epggrab_module_grab_spawn(void * m)376 char *epggrab_module_grab_spawn ( void *m )
377 {
378 int rd = -1, outlen;
379 char *outbuf;
380 epggrab_module_int_t *mod = m;
381 char **argv = NULL;
382 char *path;
383
384 /* Debug */
385 tvhinfo(mod->subsys, "%s: grab %s", mod->id, mod->path);
386
387 /* Extra arguments */
388 if (mod->args && mod->args[0]) {
389 path = alloca(strlen(mod->path) + strlen(mod->args) + 2);
390 strcpy(path, mod->path);
391 strcat(path, " ");
392 strcat(path, mod->args);
393 } else {
394 path = (char *)mod->path;
395 }
396
397 /* Arguments */
398 if (spawn_parse_args(&argv, 64, path, NULL)) {
399 tvherror(mod->subsys, "%s: unable to parse arguments", mod->id);
400 return NULL;
401 }
402
403 /* Grab */
404 outlen = spawn_and_give_stdout(argv[0], argv, NULL, &rd, NULL, 1);
405
406 spawn_free_args(argv);
407
408 if (outlen < 0)
409 goto error;
410
411 outlen = file_readall(rd, &outbuf);
412 if (outlen < 1)
413 goto error;
414
415 close(rd);
416
417 return outbuf;
418
419 error:
420 if (rd >= 0)
421 close(rd);
422 tvherror(mod->subsys, "%s: no output detected", mod->id);
423 return NULL;
424 }
425
426
427
epggrab_module_trans_xml(void * m,char * c)428 htsmsg_t *epggrab_module_trans_xml ( void *m, char *c )
429 {
430 htsmsg_t *ret;
431 char errbuf[100];
432 epggrab_module_t *mod = m;
433
434 if (!c) return NULL;
435
436 /* Extract */
437 ret = htsmsg_xml_deserialize(c, errbuf, sizeof(errbuf));
438 if (!ret)
439 tvherror(mod->subsys, "%s: htsmsg_xml_deserialize error %s", mod->id, errbuf);
440 return ret;
441 }
442
443 /* **************************************************************************
444 * External module routines
445 * *************************************************************************/
446
447 /*
448 * Socket handler
449 */
_epggrab_socket_handler(epggrab_module_ext_t * mod,int s)450 static void _epggrab_socket_handler ( epggrab_module_ext_t *mod, int s )
451 {
452 size_t outlen;
453 char *outbuf;
454 time_t tm1, tm2;
455 htsmsg_t *data = NULL;
456
457 /* Grab/Translate */
458 time(&tm1);
459 outlen = file_readall(s, &outbuf);
460 if (outlen) data = mod->trans(mod, outbuf);
461 time(&tm2);
462
463 /* Process */
464 if ( data ) {
465 tvhinfo(mod->subsys, "%s: grab took %"PRItime_t" seconds", mod->id, tm2 - tm1);
466 epggrab_module_parse(mod, data);
467
468 /* Failed */
469 } else {
470 tvherror(mod->subsys, "%s: failed to read data", mod->id);
471 }
472 }
473
474
475 /*
476 * External (socket) grab thread
477 */
_epggrab_socket_thread(void * p)478 static void *_epggrab_socket_thread ( void *p )
479 {
480 int s;
481 epggrab_module_ext_t *mod = (epggrab_module_ext_t*)p;
482 tvhinfo(mod->subsys, "%s: external socket enabled", mod->id);
483
484 while ( mod->enabled && mod->sock ) {
485 tvhdebug(mod->subsys, "%s: waiting for connection", mod->id);
486 s = accept(mod->sock, NULL, NULL);
487 if (s <= 0) continue;
488 tvhdebug(mod->subsys, "%s: got connection %d", mod->id, s);
489 _epggrab_socket_handler(mod, s);
490 close(s);
491 }
492 tvhdebug(mod->subsys, "%s: terminated", mod->id);
493 return NULL;
494 }
495
496 /*
497 * Shutdown socket module
498 */
499 static void
epggrab_module_done_socket(void * m)500 epggrab_module_done_socket( void *m )
501 {
502 epggrab_module_ext_t *mod = (epggrab_module_ext_t*)m;
503 int sock;
504
505 assert(mod->type == EPGGRAB_EXT);
506 mod->active = 0;
507 sock = mod->sock;
508 mod->sock = 0;
509 shutdown(sock, SHUT_RDWR);
510 close(sock);
511 if (mod->tid) {
512 pthread_kill(mod->tid, SIGQUIT);
513 pthread_join(mod->tid, NULL);
514 }
515 mod->tid = 0;
516 if (mod->path)
517 unlink(mod->path);
518 epggrab_module_int_done(mod);
519 }
520
521 /*
522 * Activate socket module
523 */
524 static int
epggrab_module_activate_socket(void * m,int a)525 epggrab_module_activate_socket ( void *m, int a )
526 {
527 pthread_attr_t tattr;
528 struct sockaddr_un addr;
529 epggrab_module_ext_t *mod = (epggrab_module_ext_t*)m;
530 const char *path;
531 assert(mod->type == EPGGRAB_EXT);
532
533 /* Ignore */
534 if ( mod->active == a ) return 0;
535
536 /* Disable */
537 if (!a) {
538 path = strdup(mod->path);
539 epggrab_module_done_socket(m);
540 mod->path = path;
541 /* Enable */
542 } else {
543 unlink(mod->path); // just in case!
544 hts_settings_makedirs(mod->path);
545
546 mod->sock = socket(AF_UNIX, SOCK_STREAM, 0);
547 assert(mod->sock);
548
549 memset(&addr, 0, sizeof(struct sockaddr_un));
550 addr.sun_family = AF_UNIX;
551 strlcpy(addr.sun_path, mod->path, sizeof(addr.sun_path));
552 if ( bind(mod->sock, (struct sockaddr*)&addr,
553 sizeof(struct sockaddr_un)) != 0 ) {
554 tvherror(mod->subsys, "%s: failed to bind socket", mod->id);
555 close(mod->sock);
556 mod->sock = 0;
557 return 0;
558 }
559
560 if ( listen(mod->sock, 5) != 0 ) {
561 tvherror(mod->subsys, "%s: failed to listen on socket", mod->id);
562 close(mod->sock);
563 mod->sock = 0;
564 return 0;
565 }
566
567 tvhdebug(mod->subsys, "%s: starting socket thread", mod->id);
568 pthread_attr_init(&tattr);
569 mod->active = 1;
570 tvhthread_create(&mod->tid, &tattr, _epggrab_socket_thread, mod, "epggrabso");
571 }
572 return 1;
573 }
574
575 /*
576 * Create a module
577 */
epggrab_module_ext_create(epggrab_module_ext_t * skel,const idclass_t * cls,const char * id,int subsys,const char * saveid,const char * name,int priority,const char * sockid,int (* parse)(void * m,htsmsg_t * data,epggrab_stats_t * sta),htsmsg_t * (* trans)(void * mod,char * data))578 epggrab_module_ext_t *epggrab_module_ext_create
579 ( epggrab_module_ext_t *skel, const idclass_t *cls,
580 const char *id, int subsys, const char *saveid,
581 const char *name, int priority, const char *sockid,
582 int (*parse) (void *m, htsmsg_t *data, epggrab_stats_t *sta),
583 htsmsg_t* (*trans) (void *mod, char *data) )
584 {
585 char path[512];
586
587 /* Allocate data */
588 if (!skel) skel = calloc(1, sizeof(epggrab_module_ext_t));
589
590 /* Pass through */
591 hts_settings_buildpath(path, sizeof(path), "epggrab/%s.sock", sockid);
592 epggrab_module_int_create((epggrab_module_int_t*)skel,
593 cls ?: &epggrab_mod_ext_class,
594 id, subsys, saveid, name, priority, path,
595 NULL, parse, trans);
596
597 /* Local */
598 skel->type = EPGGRAB_EXT;
599 skel->activate = epggrab_module_activate_socket;
600 skel->done = epggrab_module_done_socket;
601
602 return skel;
603 }
604
605 /* **************************************************************************
606 * OTA module functions
607 * *************************************************************************/
608
epggrab_module_ota_create(epggrab_module_ota_t * skel,const char * id,int subsys,const char * saveid,const char * name,int priority,epggrab_ota_module_ops_t * ops)609 epggrab_module_ota_t *epggrab_module_ota_create
610 ( epggrab_module_ota_t *skel,
611 const char *id, int subsys, const char *saveid,
612 const char *name, int priority,
613 epggrab_ota_module_ops_t *ops )
614 {
615 if (!skel) skel = calloc(1, sizeof(epggrab_module_ota_t));
616
617 /* Pass through */
618 epggrab_module_create((epggrab_module_t*)skel,
619 &epggrab_mod_ota_class,
620 id, subsys, saveid, name, priority);
621
622 /* Setup */
623 skel->type = EPGGRAB_OTA;
624 skel->activate = ops->activate;
625 skel->start = ops->start;
626 skel->done = ops->done;
627 skel->tune = ops->tune;
628 skel->opaque = ops->opaque;
629
630 return skel;
631 }
632