1 /*
2  *  Electronic Program Guide - pyepg grabber
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 #define _GNU_SOURCE
20 #include <stdio.h>
21 #include <string.h>
22 #include <stdlib.h>
23 #include <time.h>
24 #include "htsmsg_xml.h"
25 #include "tvheadend.h"
26 #include "spawn.h"
27 #include "channels.h"
28 #include "epg.h"
29 #include "epggrab.h"
30 #include "epggrab/private.h"
31 
32 /* **************************************************************************
33  * Parsing
34  * *************************************************************************/
35 
_pyepg_parse_time(const char * str,time_t * out)36 static int _pyepg_parse_time ( const char *str, time_t *out )
37 {
38   int ret = 0;
39   struct tm tm;
40   tm.tm_isdst = 0;
41   if ( strptime(str, "%F %T %z", &tm) != NULL ) {
42     *out = mktime(&tm);
43     ret  = 1;
44   }
45   return ret;
46 }
47 
48 static epg_genre_list_t
_pyepg_parse_genre(htsmsg_t * tags)49 *_pyepg_parse_genre ( htsmsg_t *tags )
50 {
51   htsmsg_t *e;
52   htsmsg_field_t *f;
53   epg_genre_list_t *egl = NULL;
54   HTSMSG_FOREACH(f, tags) {
55     if (!strcmp(f->hmf_name, "genre") && (e = htsmsg_get_map_by_field(f))) {
56       if (!egl) { egl = calloc(1, sizeof(epg_genre_list_t)); printf("alloc %p\n", egl); }
57       epg_genre_list_add_by_str(egl, htsmsg_get_str(e, "cdata"), NULL);
58     }
59   }
60   return egl;
61 }
62 
_pyepg_parse_channel(epggrab_module_t * mod,htsmsg_t * data,epggrab_stats_t * stats)63 static int _pyepg_parse_channel
64   ( epggrab_module_t *mod, htsmsg_t *data, epggrab_stats_t *stats )
65 {
66   int save = 0;
67   epggrab_channel_t *ch;
68   htsmsg_t *attr, *tags;
69   const char *str;
70   uint32_t u32;
71 
72   if ( data == NULL ) return 0;
73 
74   if ((attr    = htsmsg_get_map(data, "attrib")) == NULL) return 0;
75   if ((str     = htsmsg_get_str(attr, "id")) == NULL) return 0;
76   if ((tags    = htsmsg_get_map(data, "tags")) == NULL) return 0;
77   if (!(ch     = epggrab_channel_find(mod, str, 1, &save))) return 0;
78   stats->channels.total++;
79   if (save) stats->channels.created++;
80 
81   /* Update data */
82   if ((str = htsmsg_xml_get_cdata_str(tags, "name")))
83     save |= epggrab_channel_set_name(ch, str);
84   if ((str = htsmsg_xml_get_cdata_str(tags, "image")))
85     save |= epggrab_channel_set_icon(ch, str);
86   if ((!htsmsg_xml_get_cdata_u32(tags, "number", &u32)))
87     save |= epggrab_channel_set_number(ch, u32, 0);
88 
89   /* Update */
90   if (save)
91     stats->channels.modified++;
92 
93   return save;
94 }
95 
_pyepg_parse_brand(epggrab_module_t * mod,htsmsg_t * data,epggrab_stats_t * stats)96 static int _pyepg_parse_brand
97   ( epggrab_module_t *mod, htsmsg_t *data, epggrab_stats_t *stats )
98 {
99   int save = 0;
100   htsmsg_t *attr, *tags;
101   epg_brand_t *brand;
102   const char *str;
103   lang_str_t *ls;
104   uint32_t u32, changes = 0;
105 
106   if ( data == NULL ) return 0;
107 
108   if ((attr = htsmsg_get_map(data, "attrib")) == NULL) return 0;
109   if ((str  = htsmsg_get_str(attr, "id")) == NULL) return 0;
110   if ((tags = htsmsg_get_map(data, "tags")) == NULL) return 0;
111 
112   /* Find brand */
113   if ((brand = epg_brand_find_by_uri(str, mod, 1, &save, &changes)) == NULL)
114     return 0;
115   stats->brands.total++;
116   if (save) stats->brands.created++;
117 
118   /* Set title */
119   if ((str = htsmsg_xml_get_cdata_str(tags, "title"))) {
120     ls = lang_str_create2(str, NULL);
121     save |= epg_brand_set_title(brand, ls, &changes);
122     lang_str_destroy(ls);
123   }
124 
125   /* Set summary */
126   if ((str = htsmsg_xml_get_cdata_str(tags, "summary"))) {
127     ls = lang_str_create2(str, NULL);
128     save |= epg_brand_set_summary(brand, ls, &changes);
129     lang_str_destroy(ls);
130   }
131 
132   /* Set image */
133   if ((str = htsmsg_xml_get_cdata_str(tags, "image"))) {
134     ls = lang_str_create2(str, NULL);
135     save |= epg_brand_set_image(brand, str, &changes);
136     lang_str_destroy(ls);
137   } else if ((str = htsmsg_xml_get_cdata_str(tags, "thumb"))) {
138     ls = lang_str_create2(str, NULL);
139     save |= epg_brand_set_image(brand, str, &changes);
140     lang_str_destroy(ls);
141   }
142 
143   /* Set season count */
144   if (htsmsg_xml_get_cdata_u32(tags, "series-count", &u32) == 0) {
145     save |= epg_brand_set_season_count(brand, u32, &changes);
146   }
147 
148   save |= epg_brand_change_finish(brand, changes, 0);
149   if (save) stats->brands.modified++;
150 
151   return save;
152 }
153 
_pyepg_parse_season(epggrab_module_t * mod,htsmsg_t * data,epggrab_stats_t * stats)154 static int _pyepg_parse_season
155   ( epggrab_module_t *mod, htsmsg_t *data, epggrab_stats_t *stats )
156 {
157   int save = 0;
158   htsmsg_t *attr, *tags;
159   epg_season_t *season;
160   epg_brand_t *brand;
161   const char *str;
162   lang_str_t *ls;
163   uint32_t u32, changes = 0;
164 
165   if ( data == NULL ) return 0;
166 
167   if ((attr = htsmsg_get_map(data, "attrib")) == NULL) return 0;
168   if ((str  = htsmsg_get_str(attr, "id")) == NULL) return 0;
169   if ((tags = htsmsg_get_map(data, "tags")) == NULL) return 0;
170 
171   /* Find series */
172   if ((season = epg_season_find_by_uri(str, mod, 1, &save, &changes)) == NULL)
173     return 0;
174   stats->seasons.total++;
175   if (save) stats->seasons.created++;
176 
177   /* Set brand */
178   if ((str = htsmsg_get_str(attr, "brand"))) {
179     if ((brand = epg_brand_find_by_uri(str, mod, 0, NULL, NULL))) {
180       save |= epg_season_set_brand(season, brand, &changes);
181     }
182   }
183 
184   /* Set summary */
185   if ((str = htsmsg_xml_get_cdata_str(tags, "summary"))) {
186     ls = lang_str_create2(str, NULL);
187     save |= epg_season_set_summary(season, ls, &changes);
188     lang_str_destroy(ls);
189   }
190 
191   /* Set image */
192   if ((str = htsmsg_xml_get_cdata_str(tags, "image")) ||
193       (str = htsmsg_xml_get_cdata_str(tags, "thumb"))) {
194     save |= epg_season_set_image(season, str, &changes);
195   }
196 
197   /* Set season number */
198   if (htsmsg_xml_get_cdata_u32(tags, "number", &u32) == 0) {
199     save |= epg_season_set_number(season, u32, &changes);
200   }
201 
202   /* Set episode count */
203   if (htsmsg_xml_get_cdata_u32(tags, "episode-count", &u32) == 0) {
204     save |= epg_season_set_episode_count(season, u32, &changes);
205   }
206 
207   save |= epg_season_change_finish(season, changes, 0);
208   if (save) stats->seasons.modified++;
209 
210   return save;
211 }
212 
_pyepg_parse_episode(epggrab_module_t * mod,htsmsg_t * data,epggrab_stats_t * stats)213 static int _pyepg_parse_episode
214   ( epggrab_module_t *mod, htsmsg_t *data, epggrab_stats_t *stats )
215 {
216   int save = 0;
217   htsmsg_t *attr, *tags;
218   epg_episode_t *episode;
219   epg_season_t *season;
220   epg_brand_t *brand;
221   const char *str;
222   lang_str_t *ls;
223   uint32_t u32, pc, pn, changes = 0;
224   epg_genre_list_t *egl;
225 
226   if ( data == NULL ) return 0;
227 
228   if ((attr = htsmsg_get_map(data, "attrib")) == NULL) return 0;
229   if ((str  = htsmsg_get_str(attr, "id")) == NULL) return 0;
230   if ((tags = htsmsg_get_map(data, "tags")) == NULL) return 0;
231 
232   /* Find episode */
233   if ((episode = epg_episode_find_by_uri(str, mod, 1, &save, &changes)) == NULL) return 0;
234   stats->episodes.total++;
235   if (save) stats->episodes.created++;
236 
237   /* Set season */
238   if ((str = htsmsg_get_str(attr, "series"))) {
239     if ((season = epg_season_find_by_uri(str, mod, 0, NULL, NULL))) {
240       save |= epg_episode_set_season(episode, season, &changes);
241     }
242   }
243 
244   /* Set brand */
245   if ((str = htsmsg_get_str(attr, "brand"))) {
246     if ((brand = epg_brand_find_by_uri(str, mod, 0, NULL, NULL))) {
247       save |= epg_episode_set_brand(episode, brand, &changes);
248     }
249   }
250 
251   /* Set title/subtitle */
252   if ((str = htsmsg_xml_get_cdata_str(tags, "title"))) {
253     ls = lang_str_create2(str, NULL);
254     save |= epg_episode_set_title(episode, ls, &changes);
255     lang_str_destroy(ls);
256   }
257   if ((str = htsmsg_xml_get_cdata_str(tags, "subtitle"))) {
258     ls = lang_str_create2(str, NULL);
259     save |= epg_episode_set_subtitle(episode, ls, &changes);
260     lang_str_destroy(ls);
261   }
262 
263   /* Set summary */
264   if ((str = htsmsg_xml_get_cdata_str(tags, "summary"))) {
265     ls = lang_str_create2(str, NULL);
266     save |= epg_episode_set_summary(episode, ls, &changes);
267     lang_str_destroy(ls);
268   }
269 
270   /* Number */
271   if (htsmsg_xml_get_cdata_u32(tags, "number", &u32) == 0) {
272     save |= epg_episode_set_number(episode, u32, &changes);
273   }
274   if (!htsmsg_xml_get_cdata_u32(tags, "part-number", &pn)) {
275     pc = 0;
276     htsmsg_xml_get_cdata_u32(tags, "part-count", &pc);
277     save |= epg_episode_set_part(episode, pn, pc, &changes);
278   }
279 
280   /* Set image */
281   if ((str = htsmsg_xml_get_cdata_str(tags, "image")) ||
282       (str = htsmsg_xml_get_cdata_str(tags, "thumb"))) {
283     save |= epg_episode_set_image(episode, str, &changes);
284   }
285 
286   /* Genre */
287   if ((egl = _pyepg_parse_genre(tags))) {
288     save |= epg_episode_set_genre(episode, egl, &changes);
289     epg_genre_list_destroy(egl);
290   }
291 
292   /* Content */
293   if ((htsmsg_get_map(tags, "blackandwhite")))
294     save |= epg_episode_set_is_bw(episode, 1, &changes);
295 
296   save |= epg_episode_change_finish(episode, changes, 0);
297   if (save) stats->episodes.modified++;
298 
299   return save;
300 }
301 
_pyepg_parse_broadcast(epggrab_module_t * mod,htsmsg_t * data,channel_t * channel,epggrab_stats_t * stats)302 static int _pyepg_parse_broadcast
303   ( epggrab_module_t *mod, htsmsg_t *data, channel_t *channel,
304     epggrab_stats_t *stats )
305 {
306   int save = 0;
307   htsmsg_t *attr, *tags;
308   epg_episode_t *episode;
309   epg_broadcast_t *broadcast;
310   const char *id, *start, *stop;
311   time_t tm_start, tm_stop;
312   uint32_t u32, changes = 0;
313 
314   if ( data == NULL || channel == NULL ) return 0;
315 
316   if ((attr    = htsmsg_get_map(data, "attrib")) == NULL) return 0;
317   if ((id      = htsmsg_get_str(attr, "episode")) == NULL) return 0;
318   if ((start   = htsmsg_get_str(attr, "start")) == NULL ) return 0;
319   if ((stop    = htsmsg_get_str(attr, "stop")) == NULL ) return 0;
320   if ((tags    = htsmsg_get_map(data, "tags")) == NULL) return 0;
321 
322   /* Parse times */
323   if (!_pyepg_parse_time(start, &tm_start)) return 0;
324   if (!_pyepg_parse_time(stop, &tm_stop)) return 0;
325 
326   /* Find broadcast */
327   broadcast = epg_broadcast_find_by_time(channel, mod, tm_start, tm_stop,
328                                          1, &save, &changes);
329   if ( broadcast == NULL ) return 0;
330   stats->broadcasts.total++;
331   if ( save ) stats->broadcasts.created++;
332 
333   /* Quality */
334   u32 = htsmsg_get_map(tags, "hd") ? 1 : 0;
335   save |= epg_broadcast_set_is_hd(broadcast, u32, &changes);
336   u32 = htsmsg_get_map(tags, "widescreen") ? 1 : 0;
337   save |= epg_broadcast_set_is_widescreen(broadcast, u32, &changes);
338   // TODO: lines, aspect
339 
340   /* Accessibility */
341   // Note: reuse XMLTV parse code as this is the same
342   xmltv_parse_accessibility(broadcast, tags, &changes);
343 
344   /* New/Repeat */
345   u32 = htsmsg_get_map(tags, "new") || htsmsg_get_map(tags, "premiere");
346   save |= epg_broadcast_set_is_new(broadcast, u32, &changes);
347   u32 = htsmsg_get_map(tags, "repeat") ? 1 : 0;
348   save |= epg_broadcast_set_is_repeat(broadcast, u32, &changes);
349 
350   /* Set episode */
351   if ((episode = epg_episode_find_by_uri(id, mod, 1, &save, &changes)) == NULL) return 0;
352   save |= epg_broadcast_set_episode(broadcast, episode, &changes);
353 
354   save |= epg_broadcast_change_finish(broadcast, changes, 0);
355   if (save) stats->broadcasts.modified++;
356 
357   return save;
358 }
359 
_pyepg_parse_schedule(epggrab_module_t * mod,htsmsg_t * data,epggrab_stats_t * stats)360 static int _pyepg_parse_schedule
361   ( epggrab_module_t *mod, htsmsg_t *data, epggrab_stats_t *stats )
362 {
363   int save = 0;
364   htsmsg_t *attr, *tags;
365   htsmsg_field_t *f;
366   channel_t *ch;
367   epggrab_channel_t *ec;
368   const char *str;
369   idnode_list_mapping_t *ilm;
370 
371   if ( data == NULL ) return 0;
372 
373   if ((attr = htsmsg_get_map(data, "attrib")) == NULL) return 0;
374   if ((str  = htsmsg_get_str(attr, "channel")) == NULL) return 0;
375   if ((ec   = epggrab_channel_find(mod, str, 0, NULL)) == NULL) return 0;
376   if ((tags = htsmsg_get_map(data, "tags")) == NULL) return 0;
377 
378   HTSMSG_FOREACH(f, tags) {
379     if (strcmp(f->hmf_name, "broadcast") == 0) {
380       ec->laststamp = gclk();
381       LIST_FOREACH(ilm, &ec->channels, ilm_in1_link) {
382         ch = (channel_t *)ilm->ilm_in2;
383         if (!ch->ch_enabled || ch->ch_epg_parent) continue;
384         save |= _pyepg_parse_broadcast(mod, htsmsg_get_map_by_field(f),
385                                        ch, stats);
386       }
387     }
388   }
389 
390   return save;
391 }
392 
_pyepg_parse_epg(epggrab_module_t * mod,htsmsg_t * data,epggrab_stats_t * stats)393 static int _pyepg_parse_epg
394   ( epggrab_module_t *mod, htsmsg_t *data, epggrab_stats_t *stats )
395 {
396   int gsave = 0, save;
397   htsmsg_t *tags;
398   htsmsg_field_t *f;
399 
400   if ((tags = htsmsg_get_map(data, "tags")) == NULL) return 0;
401 
402   pthread_mutex_lock(&global_lock);
403   epggrab_channel_begin_scan(mod);
404   pthread_mutex_unlock(&global_lock);
405 
406   HTSMSG_FOREACH(f, tags) {
407     save = 0;
408     if (strcmp(f->hmf_name, "channel") == 0 ) {
409       pthread_mutex_lock(&global_lock);
410       save = _pyepg_parse_channel(mod, htsmsg_get_map_by_field(f), stats);
411       pthread_mutex_unlock(&global_lock);
412     } else if (strcmp(f->hmf_name, "brand") == 0 ) {
413       pthread_mutex_lock(&global_lock);
414       save = _pyepg_parse_brand(mod, htsmsg_get_map_by_field(f), stats);
415       if (save) epg_updated();
416       pthread_mutex_unlock(&global_lock);
417     } else if (strcmp(f->hmf_name, "series") == 0 ) {
418       pthread_mutex_lock(&global_lock);
419       save = _pyepg_parse_season(mod, htsmsg_get_map_by_field(f), stats);
420       if (save) epg_updated();
421       pthread_mutex_unlock(&global_lock);
422     } else if (strcmp(f->hmf_name, "episode") == 0 ) {
423       pthread_mutex_lock(&global_lock);
424       save = _pyepg_parse_episode(mod, htsmsg_get_map_by_field(f), stats);
425       if (save) epg_updated();
426       pthread_mutex_unlock(&global_lock);
427     } else if (strcmp(f->hmf_name, "schedule") == 0 ) {
428       pthread_mutex_lock(&global_lock);
429       save = _pyepg_parse_schedule(mod, htsmsg_get_map_by_field(f), stats);
430       if (save) epg_updated();
431       pthread_mutex_unlock(&global_lock);
432     }
433     gsave |= save;
434   }
435 
436   pthread_mutex_lock(&global_lock);
437   epggrab_channel_end_scan(mod);
438   pthread_mutex_unlock(&global_lock);
439 
440   return gsave;
441 }
442 
_pyepg_parse(void * mod,htsmsg_t * data,epggrab_stats_t * stats)443 static int _pyepg_parse
444   ( void *mod, htsmsg_t *data, epggrab_stats_t *stats )
445 {
446   htsmsg_t *tags, *epg;
447 
448   if ((tags = htsmsg_get_map(data, "tags")) == NULL) return 0;
449 
450   /* PyEPG format */
451   if ((epg = htsmsg_get_map(tags, "epg")) != NULL)
452     return _pyepg_parse_epg(mod, epg, stats);
453 
454   return 0;
455 }
456 
457 /* ************************************************************************
458  * Module Setup
459  * ***********************************************************************/
460 
461 const idclass_t epggrab_mod_int_pyepg_class = {
462   .ic_super      = &epggrab_mod_int_class,
463   .ic_class      = "epggrab_mod_int_pyepg",
464   .ic_caption    = N_("Internal PyEPG Grabber"),
465 };
466 
467 
468 const idclass_t epggrab_mod_ext_pyepg_class = {
469   .ic_super      = &epggrab_mod_ext_class,
470   .ic_class      = "epggrab_mod_ext_pyepg",
471   .ic_caption    = N_("External PyEPG Grabber"),
472 };
473 
474 
pyepg_init(void)475 void pyepg_init ( void )
476 {
477   char buf[256];
478 
479   /* Internal module */
480   if (find_exec("pyepg", buf, sizeof(buf)-1))
481     epggrab_module_int_create(NULL, &epggrab_mod_int_pyepg_class,
482                               "pyepg-internal", LS_PYEPG, "pyepg",
483                               "PyEPG", 4, buf,
484                               NULL, _pyepg_parse, NULL);
485 
486   /* External module */
487   epggrab_module_ext_create(NULL, &epggrab_mod_ext_pyepg_class,
488                             "pyepg", LS_PYEPG, "pyepg", "PyEPG", 4, "pyepg",
489                             _pyepg_parse, NULL);
490 }
491 
pyepg_done(void)492 void pyepg_done ( void )
493 {
494 }
495 
pyepg_load(void)496 void pyepg_load ( void )
497 {
498   epggrab_module_channels_load("pyepg");
499 }
500