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