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