1 /*
2  *  EPG Grabber - channel functions
3  *  Copyright (C) 2012 Adam Sutton
4  *  Copyright (C) 2015 Jaroslav Kysela
5  *
6  *  This program is free software: you can redistribute it and/or modify
7  *  it under the terms of the GNU General Public License as published by
8  *  the Free Software Foundation, either version 3 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU General Public License for more details.
15  *
16  *  You should have received a copy of the GNU General Public License
17  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "tvheadend.h"
21 #include "settings.h"
22 #include "htsmsg.h"
23 #include "channels.h"
24 #include "epg.h"
25 #include "epggrab.h"
26 #include "epggrab/private.h"
27 
28 #include <assert.h>
29 #include <string.h>
30 
31 struct epggrab_channel_queue epggrab_channel_entries;
32 
33 SKEL_DECLARE(epggrab_channel_skel, epggrab_channel_t);
34 
35 /* **************************************************************************
36  * EPG Grab Channel functions
37  * *************************************************************************/
38 
39 static inline int
is_paired(epggrab_channel_t * ec)40 is_paired( epggrab_channel_t *ec )
41 {
42   return ec->only_one && LIST_FIRST(&ec->channels);
43 }
44 
45 static inline int
epggrab_channel_check(epggrab_channel_t * ec,channel_t * ch)46 epggrab_channel_check ( epggrab_channel_t *ec, channel_t *ch )
47 {
48   if (!ec || !ch || !ch->ch_epgauto || !ch->ch_enabled)
49     return 0;
50   if (ch->ch_epg_parent || !ec->enabled)
51     return 0;
52   if (is_paired(ec))
53     return 0; // ignore already paired
54   return 1;
55 }
56 
57 /* Check if channels match by epgid */
epggrab_channel_match_epgid(epggrab_channel_t * ec,channel_t * ch)58 int epggrab_channel_match_epgid ( epggrab_channel_t *ec, channel_t *ch )
59 {
60   const char *chid;
61 
62   if (ec->id == NULL)
63     return 0;
64   if (!epggrab_channel_check(ec, ch))
65     return 0;
66   chid = channel_get_epgid(ch);
67   if (chid && !strcmp(ec->id, chid))
68     return 1;
69   return 0;
70 }
71 
72 /* Check if channels match by name */
epggrab_channel_match_name(epggrab_channel_t * ec,channel_t * ch)73 int epggrab_channel_match_name ( epggrab_channel_t *ec, channel_t *ch )
74 {
75   const char *name, *s;
76   htsmsg_field_t *f;
77 
78   if (!epggrab_channel_check(ec, ch))
79     return 0;
80 
81   name = channel_get_name(ch);
82   if (name == NULL)
83     return 0;
84 
85   if (ec->name && !strcasecmp(ec->name, name))
86     return 1;
87 
88   if (ec->names)
89     HTSMSG_FOREACH(f, ec->names)
90       if ((s = htsmsg_field_get_str(f)) != NULL)
91         if (!strcasecmp(s, name))
92           return 1;
93 
94   return 0;
95 }
96 
97 /* Check if channels match by number */
epggrab_channel_match_number(epggrab_channel_t * ec,channel_t * ch)98 int epggrab_channel_match_number ( epggrab_channel_t *ec, channel_t *ch )
99 {
100   if (ec->lcn == 0)
101     return 0;
102 
103   if (!epggrab_channel_check(ec, ch))
104     return 0;
105 
106   if (ec->lcn && ec->lcn == channel_get_number(ch)) return 1;
107   return 0;
108 }
109 
110 /* Delete ilm */
111 static void
_epgggrab_channel_link_delete(idnode_list_mapping_t * ilm,int delconf)112 _epgggrab_channel_link_delete(idnode_list_mapping_t *ilm, int delconf)
113 {
114   epggrab_channel_t *ec = (epggrab_channel_t *)ilm->ilm_in1;
115   channel_t *ch = (channel_t *)ilm->ilm_in2;
116   tvhdebug(ec->mod->subsys, "%s: unlinking %s from %s",
117            ec->mod->id, ec->id, channel_get_name(ch));
118   idnode_list_unlink(ilm, delconf ? ec : NULL);
119 }
120 
121 /* Destroy */
122 void
epggrab_channel_link_delete(epggrab_channel_t * ec,channel_t * ch,int delconf)123 epggrab_channel_link_delete
124   ( epggrab_channel_t *ec, channel_t *ch, int delconf )
125 {
126   idnode_list_mapping_t *ilm;
127   LIST_FOREACH(ilm, &ec->channels, ilm_in1_link)
128     if (ilm->ilm_in1 == &ec->idnode && ilm->ilm_in2 == &ch->ch_id) {
129       _epgggrab_channel_link_delete(ilm, delconf);
130       return;
131     }
132 }
133 
134 /* Destroy all links */
epggrab_channel_links_delete(epggrab_channel_t * ec,int delconf)135 static void epggrab_channel_links_delete( epggrab_channel_t *ec, int delconf )
136 {
137   idnode_list_mapping_t *ilm;
138   while ((ilm = LIST_FIRST(&ec->channels)))
139     _epgggrab_channel_link_delete(ilm, delconf);
140 }
141 
142 /* Do update */
143 static void
epggrab_channel_sync(epggrab_channel_t * ec,channel_t * ch)144 epggrab_channel_sync( epggrab_channel_t *ec, channel_t *ch )
145 {
146   int save = 0;
147 
148   if (ec->update_chname && ec->name && epggrab_conf.channel_rename)
149     save |= channel_set_name(ch, ec->name);
150   if (ec->update_chnum && ec->lcn > 0 && epggrab_conf.channel_renumber)
151     save |= channel_set_number(ch, ec->lcn / CHANNEL_SPLIT, ec->lcn % CHANNEL_SPLIT);
152   if (ec->update_chicon && ec->icon && epggrab_conf.channel_reicon)
153     save |= channel_set_icon(ch, ec->icon);
154   if (save)
155     idnode_changed(&ch->ch_id);
156 }
157 
158 /* Link epggrab channel to real channel */
159 int
epggrab_channel_link(epggrab_channel_t * ec,channel_t * ch,void * origin)160 epggrab_channel_link ( epggrab_channel_t *ec, channel_t *ch, void *origin )
161 {
162   idnode_list_mapping_t *ilm;
163 
164   /* No change */
165   if (!ch || !ch->ch_enabled) return 0;
166 
167   /* Already linked */
168   LIST_FOREACH(ilm, &ec->channels, ilm_in1_link)
169     if (ilm->ilm_in2 == &ch->ch_id) {
170       ilm->ilm_mark = 0;
171       epggrab_channel_sync(ec, ch);
172       return 0;
173     }
174 
175   /* New link */
176   tvhdebug(ec->mod->subsys, "%s: linking %s to %s",
177            ec->mod->id, ec->id, channel_get_name(ch));
178 
179   ilm = idnode_list_link(&ec->idnode, &ec->channels,
180                          &ch->ch_id, &ch->ch_epggrab,
181                          origin, 1);
182   if (ilm == NULL)
183     return 0;
184 
185   epggrab_channel_sync(ec, ch);
186 
187   if (origin == NULL)
188     idnode_changed(&ec->idnode);
189   return 1;
190 }
191 
192 int
epggrab_channel_map(idnode_t * ec,idnode_t * ch,void * origin)193 epggrab_channel_map ( idnode_t *ec, idnode_t *ch, void *origin )
194 {
195   return epggrab_channel_link((epggrab_channel_t *)ec, (channel_t *)ch, origin);
196 }
197 
198 /* Set name */
epggrab_channel_set_name(epggrab_channel_t * ec,const char * name)199 int epggrab_channel_set_name ( epggrab_channel_t *ec, const char *name )
200 {
201   idnode_list_mapping_t *ilm;
202   channel_t *ch;
203   int save = 0;
204   if (!ec || !name) return 0;
205   if (!ec->newnames && (!ec->name || strcmp(ec->name, name))) {
206     if (ec->name) free(ec->name);
207     ec->name = strdup(name);
208     if (epggrab_conf.channel_rename) {
209       LIST_FOREACH(ilm, &ec->channels, ilm_in1_link) {
210         ch = (channel_t *)ilm->ilm_in2;
211         if (channel_set_name(ch, name))
212           idnode_changed(&ch->ch_id);
213       }
214     }
215     save = 1;
216   }
217   if (ec->newnames == NULL)
218     ec->newnames = htsmsg_create_list();
219   htsmsg_add_str(ec->newnames, NULL, name);
220   ec->updated |= save;
221   return save;
222 }
223 
224 /* Set icon */
epggrab_channel_set_icon(epggrab_channel_t * ec,const char * icon)225 int epggrab_channel_set_icon ( epggrab_channel_t *ec, const char *icon )
226 {
227   idnode_list_mapping_t *ilm;
228   channel_t *ch;
229   int save = 0;
230   if (!ec || !icon) return 0;
231   if (!ec->icon || strcmp(ec->icon, icon) ) {
232     if (ec->icon) free(ec->icon);
233     ec->icon = strdup(icon);
234     if (epggrab_conf.channel_reicon) {
235       LIST_FOREACH(ilm, &ec->channels, ilm_in1_link) {
236         ch = (channel_t *)ilm->ilm_in2;
237         if (channel_set_icon(ch, icon))
238           idnode_changed(&ch->ch_id);
239       }
240     }
241     save = 1;
242   }
243   ec->updated |= save;
244   return save;
245 }
246 
247 /* Set channel number */
epggrab_channel_set_number(epggrab_channel_t * ec,int major,int minor)248 int epggrab_channel_set_number ( epggrab_channel_t *ec, int major, int minor )
249 {
250   idnode_list_mapping_t *ilm;
251   channel_t *ch;
252   int64_t lcn;
253   int save = 0;
254   if (!ec || (major <= 0 && minor <= 0)) return 0;
255   lcn = (major * CHANNEL_SPLIT) + minor;
256   if (ec->lcn != lcn) {
257     ec->lcn = lcn;
258     if (epggrab_conf.channel_renumber) {
259       LIST_FOREACH(ilm, &ec->channels, ilm_in1_link) {
260         ch = (channel_t *)ilm->ilm_in2;
261         if (channel_set_number(ch,
262                                lcn / CHANNEL_SPLIT,
263                                lcn % CHANNEL_SPLIT))
264           idnode_changed(&ch->ch_id);
265       }
266     }
267     save = 1;
268   }
269   ec->updated |= save;
270   return save;
271 }
272 
273 /* Autolink EPG channel to channel */
274 static int
epggrab_channel_autolink_one(epggrab_channel_t * ec,channel_t * ch)275 epggrab_channel_autolink_one( epggrab_channel_t *ec, channel_t *ch )
276 {
277   if (epggrab_channel_match_epgid(ec, ch))
278     return epggrab_channel_link(ec, ch, NULL);
279   else if (epggrab_channel_match_name(ec, ch))
280     return epggrab_channel_link(ec, ch, NULL);
281   else if (epggrab_channel_match_number(ec, ch))
282     return epggrab_channel_link(ec, ch, NULL);
283   return 0;
284 }
285 
286 /* Autolink EPG channel to channel */
287 static int
epggrab_channel_autolink(epggrab_channel_t * ec)288 epggrab_channel_autolink( epggrab_channel_t *ec )
289 {
290   channel_t *ch;
291 
292   CHANNEL_FOREACH(ch)
293     if (epggrab_channel_match_epgid(ec, ch))
294       if (epggrab_channel_link(ec, ch, NULL))
295         return 1;
296   CHANNEL_FOREACH(ch)
297     if (epggrab_channel_match_name(ec, ch))
298       if (epggrab_channel_link(ec, ch, NULL))
299         return 1;
300   CHANNEL_FOREACH(ch)
301     if (epggrab_channel_match_number(ec, ch))
302       if (epggrab_channel_link(ec, ch, NULL))
303         return 1;
304   return 0;
305 }
306 
307 /* Channel settings updated */
epggrab_channel_updated(epggrab_channel_t * ec)308 void epggrab_channel_updated ( epggrab_channel_t *ec )
309 {
310   if (!ec) return;
311 
312   /* Find a link */
313   if (!is_paired(ec))
314     epggrab_channel_autolink(ec);
315 
316   /* Save */
317   idnode_changed(&ec->idnode);
318 }
319 
320 /* ID comparison */
_ch_id_cmp(void * a,void * b)321 static int _ch_id_cmp ( void *a, void *b )
322 {
323   return strcmp(((epggrab_channel_t*)a)->id,
324                 ((epggrab_channel_t*)b)->id);
325 }
326 
327 /* Create new entry */
epggrab_channel_create(epggrab_module_t * owner,htsmsg_t * conf,const char * uuid)328 epggrab_channel_t *epggrab_channel_create
329   ( epggrab_module_t *owner, htsmsg_t *conf, const char *uuid )
330 {
331   epggrab_channel_t *ec;
332 
333   ec = calloc(1, sizeof(*ec));
334   if (idnode_insert(&ec->idnode, uuid, &epggrab_channel_class, 0)) {
335     if (uuid)
336       tvherror(LS_EPGGRAB, "invalid uuid '%s'", uuid);
337     free(ec);
338     return NULL;
339   }
340 
341   ec->mod = owner;
342   ec->enabled = 1;
343   ec->update_chicon = 1;
344   ec->update_chnum = 1;
345   ec->update_chname = 1;
346 
347   if (conf)
348     idnode_load(&ec->idnode, conf);
349 
350   if (ec->id == NULL)
351     ec->id = strdup("");
352 
353   TAILQ_INSERT_TAIL(&epggrab_channel_entries, ec, all_link);
354   if (RB_INSERT_SORTED(&owner->channels, ec, link, _ch_id_cmp)) {
355     tvherror(LS_EPGGRAB, "removing duplicate channel id '%s' (uuid '%s')", ec->id, uuid);
356     epggrab_channel_destroy(ec, 1, 0);
357     return NULL;
358   }
359 
360   return ec;
361 }
362 
363 /* Find/Create channel in the list */
epggrab_channel_find(epggrab_module_t * mod,const char * id,int create,int * save)364 epggrab_channel_t *epggrab_channel_find
365   ( epggrab_module_t *mod, const char *id, int create, int *save )
366 {
367   char *s;
368   epggrab_channel_t *ec;
369 
370   if (id == NULL || id[0] == '\0') {
371     tvhwarn(LS_EPGGRAB, "%s: ignoring empty EPG id source", mod->id);
372     return NULL;
373   }
374 
375   SKEL_ALLOC(epggrab_channel_skel);
376   s = epggrab_channel_skel->id = tvh_strdupa(id);
377 
378   /* Replace / with # */
379   // Note: this is a bit of a nasty fix for #1774, but will do for now
380   while (*s) {
381     if (*s == '/') *s = '#';
382     s++;
383   }
384 
385   /* Find */
386   if (!create) {
387     ec = RB_FIND(&mod->channels, epggrab_channel_skel, link, _ch_id_cmp);
388 
389   /* Find/Create */
390   } else {
391     ec = RB_INSERT_SORTED(&mod->channels, epggrab_channel_skel, link, _ch_id_cmp);
392     if (!ec) {
393       ec       = epggrab_channel_skel;
394       SKEL_USED(epggrab_channel_skel);
395       ec->enabled = 1;
396       ec->id   = strdup(ec->id);
397       ec->mod  = mod;
398       TAILQ_INSERT_TAIL(&epggrab_channel_entries, ec, all_link);
399 
400       if (idnode_insert(&ec->idnode, NULL, &epggrab_channel_class, 0))
401         abort();
402       *save    = 1;
403       return ec;
404     }
405   }
406   return ec;
407 }
408 
epggrab_channel_destroy(epggrab_channel_t * ec,int delconf,int rb_remove)409 void epggrab_channel_destroy( epggrab_channel_t *ec, int delconf, int rb_remove )
410 {
411   char ubuf[UUID_HEX_SIZE];
412 
413   if (ec == NULL) return;
414 
415   idnode_save_check(&ec->idnode, delconf);
416 
417   /* Already linked */
418   epggrab_channel_links_delete(ec, 1);
419   if (rb_remove)
420     RB_REMOVE(&ec->mod->channels, ec, link);
421   TAILQ_REMOVE(&epggrab_channel_entries, ec, all_link);
422   idnode_unlink(&ec->idnode);
423 
424   if (delconf)
425     hts_settings_remove("epggrab/%s/channels/%s",
426                         ec->mod->saveid, idnode_uuid_as_str(&ec->idnode, ubuf));
427 
428   htsmsg_destroy(ec->newnames);
429   htsmsg_destroy(ec->names);
430   free(ec->comment);
431   free(ec->name);
432   free(ec->icon);
433   free(ec->id);
434   free(ec);
435 }
436 
epggrab_channel_flush(epggrab_module_t * mod,int delconf)437 void epggrab_channel_flush
438   ( epggrab_module_t *mod, int delconf )
439 {
440   epggrab_channel_t *ec;
441   while ((ec = RB_FIRST(&mod->channels)) != NULL)
442     epggrab_channel_destroy(ec, delconf, 1);
443 }
444 
epggrab_channel_begin_scan(epggrab_module_t * mod)445 void epggrab_channel_begin_scan ( epggrab_module_t *mod )
446 {
447   epggrab_channel_t *ec;
448   lock_assert(&global_lock);
449   RB_FOREACH(ec, &mod->channels, link) {
450     ec->updated = 0;
451     if (ec->newnames) {
452       htsmsg_destroy(ec->newnames);
453       ec->newnames = NULL;
454     }
455   }
456 }
457 
epggrab_channel_end_scan(epggrab_module_t * mod)458 void epggrab_channel_end_scan ( epggrab_module_t *mod )
459 {
460   epggrab_channel_t *ec;
461   lock_assert(&global_lock);
462   RB_FOREACH(ec, &mod->channels, link) {
463     if (ec->newnames) {
464       if (htsmsg_cmp(ec->names, ec->newnames)) {
465         htsmsg_destroy(ec->names);
466         ec->names = ec->newnames;
467         ec->updated = 1;
468       } else {
469         htsmsg_destroy(ec->newnames);
470       }
471       ec->newnames = NULL;
472     }
473     if (ec->updated) {
474       epggrab_channel_updated(ec);
475       ec->updated = 0;
476     }
477   }
478 }
479 
480 /* **************************************************************************
481  * Global routines
482  * *************************************************************************/
483 
epggrab_channel_add(channel_t * ch)484 void epggrab_channel_add ( channel_t *ch )
485 {
486   epggrab_module_t *mod;
487   epggrab_channel_t *ec;
488 
489   LIST_FOREACH(mod, &epggrab_modules, link)
490     RB_FOREACH(ec, &mod->channels, link) {
491       if (!is_paired(ec))
492         epggrab_channel_autolink_one(ec, ch);
493     }
494 }
495 
epggrab_channel_rem(channel_t * ch)496 void epggrab_channel_rem ( channel_t *ch )
497 {
498   idnode_list_mapping_t *ilm;
499 
500   while ((ilm = LIST_FIRST(&ch->ch_epggrab)) != NULL)
501     idnode_list_unlink(ilm, ch);
502 }
503 
epggrab_channel_mod(channel_t * ch)504 void epggrab_channel_mod ( channel_t *ch )
505 {
506   return epggrab_channel_add(ch);
507 }
508 
509 const char *
epggrab_channel_get_id(epggrab_channel_t * ec)510 epggrab_channel_get_id ( epggrab_channel_t *ec )
511 {
512   static char buf[1024];
513   epggrab_module_t *m = ec->mod;
514   snprintf(buf, sizeof(buf), "%s|%s", m->id, ec->id);
515   return buf;
516 }
517 
518 epggrab_channel_t *
epggrab_channel_find_by_id(const char * id)519 epggrab_channel_find_by_id ( const char *id )
520 {
521   char buf[1024];
522   char *mid, *cid;
523   epggrab_module_t *mod;
524   strlcpy(buf, id, sizeof(buf));
525   if ((mid = strtok_r(buf, "|", &cid)) && cid)
526     if ((mod = epggrab_module_find_by_id(mid)) != NULL)
527       return epggrab_channel_find(mod, cid, 0, NULL);
528   return NULL;
529 }
530 
531 int
epggrab_channel_is_ota(epggrab_channel_t * ec)532 epggrab_channel_is_ota ( epggrab_channel_t *ec )
533 {
534   return ec->mod->type == EPGGRAB_OTA;
535 }
536 
537 /*
538  * Class
539  */
540 
541 static const char *
epggrab_channel_class_get_title(idnode_t * self,const char * lang)542 epggrab_channel_class_get_title(idnode_t *self, const char *lang)
543 {
544   epggrab_channel_t *ec = (epggrab_channel_t*)self;
545 
546   snprintf(prop_sbuf, PROP_SBUF_LEN, "%s: %s (%s)",
547            ec->name ?: ec->id, ec->id, ec->mod->name);
548   return prop_sbuf;
549 }
550 
551 static htsmsg_t *
epggrab_channel_class_save(idnode_t * self,char * filename,size_t fsize)552 epggrab_channel_class_save(idnode_t *self, char *filename, size_t fsize)
553 {
554   epggrab_channel_t *ec = (epggrab_channel_t *)self;
555   htsmsg_t *m = htsmsg_create_map();
556   char ubuf[UUID_HEX_SIZE];
557   idnode_save(&ec->idnode, m);
558   snprintf(filename, fsize, "epggrab/%s/channels/%s",
559            ec->mod->saveid, idnode_uuid_as_str(&ec->idnode, ubuf));
560   return m;
561 }
562 
563 static void
epggrab_channel_class_delete(idnode_t * self)564 epggrab_channel_class_delete(idnode_t *self)
565 {
566   epggrab_channel_destroy((epggrab_channel_t *)self, 1, 1);
567 }
568 
569 static const void *
epggrab_channel_class_modid_get(void * obj)570 epggrab_channel_class_modid_get ( void *obj )
571 {
572   epggrab_channel_t *ec = obj;
573   snprintf(prop_sbuf, PROP_SBUF_LEN, "%s", ec->mod->id ?: "");
574   return &prop_sbuf_ptr;
575 }
576 
577 static int
epggrab_channel_class_modid_set(void * obj,const void * p)578 epggrab_channel_class_modid_set ( void *obj, const void *p )
579 {
580   return 0;
581 }
582 
583 static const void *
epggrab_channel_class_module_get(void * obj)584 epggrab_channel_class_module_get ( void *obj )
585 {
586   epggrab_channel_t *ec = obj;
587   snprintf(prop_sbuf, PROP_SBUF_LEN, "%s", ec->mod->name ?: "");
588   return &prop_sbuf_ptr;
589 }
590 
591 static const void *
epggrab_channel_class_path_get(void * obj)592 epggrab_channel_class_path_get ( void *obj )
593 {
594   epggrab_channel_t *ec = obj;
595   if (ec->mod->type == EPGGRAB_INT || ec->mod->type == EPGGRAB_EXT)
596     snprintf(prop_sbuf, PROP_SBUF_LEN, "%s", ((epggrab_module_int_t *)ec->mod)->path ?: "");
597   else
598     prop_sbuf[0] = '\0';
599   return &prop_sbuf_ptr;
600 }
601 
602 static const void *
epggrab_channel_class_names_get(void * obj)603 epggrab_channel_class_names_get ( void *obj )
604 {
605   epggrab_channel_t *ec = obj;
606   char *s = ec->names ? htsmsg_list_2_csv(ec->names, ',', 0) : NULL;
607   snprintf(prop_sbuf, PROP_SBUF_LEN, "%s", s ?: "");
608   free(s);
609   return &prop_sbuf_ptr;
610 }
611 
612 static int
epggrab_channel_class_names_set(void * obj,const void * p)613 epggrab_channel_class_names_set ( void *obj, const void *p )
614 {
615   htsmsg_t *m = htsmsg_csv_2_list(p, ',');
616   epggrab_channel_t *ec = obj;
617   if (htsmsg_cmp(ec->names, m)) {
618     htsmsg_destroy(ec->names);
619     ec->names = m;
620   } else {
621     htsmsg_destroy(m);
622   }
623   return 0;
624 }
625 
626 static void
epggrab_channel_class_enabled_notify(void * obj,const char * lang)627 epggrab_channel_class_enabled_notify ( void *obj, const char *lang )
628 {
629   epggrab_channel_t *ec = obj;
630   if (!ec->enabled) {
631     epggrab_channel_links_delete(ec, 1);
632   } else {
633     epggrab_channel_updated(ec);
634   }
635 }
636 
637 static const void *
epggrab_channel_class_channels_get(void * obj)638 epggrab_channel_class_channels_get ( void *obj )
639 {
640   epggrab_channel_t *ec = obj;
641   return idnode_list_get1(&ec->channels);
642 }
643 
644 static int
epggrab_channel_class_channels_set(void * obj,const void * p)645 epggrab_channel_class_channels_set ( void *obj, const void *p )
646 {
647   epggrab_channel_t *ec = obj;
648   return idnode_list_set1(&ec->idnode, &ec->channels,
649                           &channel_class, (htsmsg_t *)p,
650                           epggrab_channel_map);
651 }
652 
653 static char *
epggrab_channel_class_channels_rend(void * obj,const char * lang)654 epggrab_channel_class_channels_rend ( void *obj, const char *lang )
655 {
656   epggrab_channel_t *ec = obj;
657   return idnode_list_get_csv1(&ec->channels, lang);
658 }
659 
660 static idnode_slist_t epggrab_channel_class_update_slist[] = {
661   {
662     .id   = "update_icon",
663     .name = N_("Icon"),
664     .off  = offsetof(epggrab_channel_t, update_chicon),
665   },
666   {
667     .id   = "update_chnum",
668     .name = N_("Number"),
669     .off  = offsetof(epggrab_channel_t, update_chnum),
670   },
671   {
672     .id   = "update_chname",
673     .name = N_("Name"),
674     .off  = offsetof(epggrab_channel_t, update_chname),
675   },
676   {}
677 };
678 
679 static htsmsg_t *
epggrab_channel_class_update_enum(void * obj,const char * lang)680 epggrab_channel_class_update_enum ( void *obj, const char *lang )
681 {
682   return idnode_slist_enum(obj, epggrab_channel_class_update_slist, lang);
683 }
684 
685 static const void *
epggrab_channel_class_update_get(void * obj)686 epggrab_channel_class_update_get ( void *obj )
687 {
688   return idnode_slist_get(obj, epggrab_channel_class_update_slist);
689 }
690 
691 static char *
epggrab_channel_class_update_rend(void * obj,const char * lang)692 epggrab_channel_class_update_rend ( void *obj, const char *lang )
693 {
694   return idnode_slist_rend(obj, epggrab_channel_class_update_slist, lang);
695 }
696 
697 static int
epggrab_channel_class_update_set(void * obj,const void * p)698 epggrab_channel_class_update_set ( void *obj, const void *p )
699 {
700   return idnode_slist_set(obj, epggrab_channel_class_update_slist, p);
701 }
702 
703 static void
epggrab_channel_class_update_notify(void * obj,const char * lang)704 epggrab_channel_class_update_notify ( void *obj, const char *lang )
705 {
706   epggrab_channel_t *ec = obj;
707   channel_t *ch;
708   idnode_list_mapping_t *ilm;
709 
710   if (!ec->update_chicon && !ec->update_chnum && !ec->update_chname)
711     return;
712   LIST_FOREACH(ilm, &ec->channels, ilm_in1_link) {
713     ch = (channel_t *)ilm->ilm_in2;
714     epggrab_channel_sync(ec, ch);
715   }
716 }
717 
718 static void
epggrab_channel_class_only_one_notify(void * obj,const char * lang)719 epggrab_channel_class_only_one_notify ( void *obj, const char *lang )
720 {
721   epggrab_channel_t *ec = obj;
722   channel_t *ch, *first = NULL;
723   idnode_list_mapping_t *ilm1, *ilm2;
724   if (ec->only_one) {
725     for(ilm1 = LIST_FIRST(&ec->channels); ilm1; ilm1 = ilm2) {
726       ilm2 = LIST_NEXT(ilm1, ilm_in1_link);
727       ch = (channel_t *)ilm1->ilm_in2;
728       if (!first)
729         first = ch;
730       else if (ch->ch_epgauto && first)
731         idnode_list_unlink(ilm1, ec);
732     }
733   } else {
734     epggrab_channel_updated(ec);
735   }
736 }
737 
738 CLASS_DOC(epggrabber_channel)
739 
740 const idclass_t epggrab_channel_class = {
741   .ic_class      = "epggrab_channel",
742   .ic_caption    = N_("EPG Grabber Channel"),
743   .ic_doc        = tvh_doc_epggrabber_channel_class,
744   .ic_event      = "epggrab_channel",
745   .ic_perm_def   = ACCESS_ADMIN,
746   .ic_save       = epggrab_channel_class_save,
747   .ic_get_title  = epggrab_channel_class_get_title,
748   .ic_delete     = epggrab_channel_class_delete,
749   .ic_groups     = (const property_group_t[]) {
750     {
751       .name   = N_("Configuration"),
752       .number = 1,
753     },
754     {}
755   },
756   .ic_properties = (const property_t[]){
757     {
758       .type     = PT_BOOL,
759       .id       = "enabled",
760       .name     = N_("Enabled"),
761       .desc     = N_("Enable/disable EPG data for the entry."),
762       .off      = offsetof(epggrab_channel_t, enabled),
763       .notify   = epggrab_channel_class_enabled_notify,
764       .group    = 1
765     },
766     {
767       .type     = PT_STR,
768       .id       = "modid",
769       .name     = N_("Module ID"),
770       .desc     = N_("Module ID used to grab EPG data."),
771       .get      = epggrab_channel_class_modid_get,
772       .set      = epggrab_channel_class_modid_set,
773       .opts     = PO_RDONLY | PO_HIDDEN,
774       .group    = 1
775     },
776     {
777       .type     = PT_STR,
778       .id       = "module",
779       .name     = N_("Module"),
780       .desc     = N_("Name of the module used to grab EPG data."),
781       .get      = epggrab_channel_class_module_get,
782       .opts     = PO_RDONLY | PO_NOSAVE,
783       .group    = 1
784     },
785     {
786       .type     = PT_STR,
787       .id       = "path",
788       .name     = N_("Path"),
789       .desc     = N_("Data path (if applicable)."),
790       .get      = epggrab_channel_class_path_get,
791       .opts     = PO_RDONLY | PO_NOSAVE,
792       .group    = 1
793     },
794     {
795       .type     = PT_TIME,
796       .id       = "updated",
797       .name     = N_("Updated"),
798       .desc     = N_("Date the EPG data was last updated (not set for OTA "
799                      "grabbers)."),
800       .off      = offsetof(epggrab_channel_t, laststamp),
801       .opts     = PO_RDONLY | PO_NOSAVE,
802       .group    = 1
803     },
804     {
805       .type     = PT_STR,
806       .id       = "id",
807       .name     = N_("ID"),
808       .desc     = N_("EPG data ID."),
809       .off      = offsetof(epggrab_channel_t, id),
810       .group    = 1
811     },
812     {
813       .type     = PT_STR,
814       .id       = "name",
815       .name     = N_("Name"),
816       .desc     = N_("Service name found in EPG data."),
817       .off      = offsetof(epggrab_channel_t, name),
818       .group    = 1
819     },
820     {
821       .type     = PT_STR,
822       .id       = "names",
823       .name     = N_("Names"),
824       .desc     = N_("Additional service names found in EPG data."),
825       .get      = epggrab_channel_class_names_get,
826       .set      = epggrab_channel_class_names_set,
827       .group    = 1
828     },
829     {
830       .type     = PT_S64,
831       .intextra = CHANNEL_SPLIT,
832       .id       = "number",
833       .name     = N_("Number"),
834       .desc     = N_("Channel number as defined in EPG data."),
835       .off      = offsetof(epggrab_channel_t, lcn),
836       .group    = 1
837     },
838     {
839       .type     = PT_STR,
840       .id       = "icon",
841       .name     = N_("Icon"),
842       .desc     = N_("Channel icon as defined in EPG data."),
843       .off      = offsetof(epggrab_channel_t, icon),
844       .group    = 1
845     },
846     {
847       .type     = PT_STR,
848       .islist   = 1,
849       .id       = "channels",
850       .name     = N_("Channels"),
851       .desc     = N_("Channels EPG data is used by."),
852       .set      = epggrab_channel_class_channels_set,
853       .get      = epggrab_channel_class_channels_get,
854       .list     = channel_class_get_list,
855       .rend     = epggrab_channel_class_channels_rend,
856       .group    = 1
857     },
858     {
859       .type     = PT_BOOL,
860       .id       = "only_one",
861       .name     = N_("Once per auto channel"),
862       .desc     = N_("Only use this EPG data once when automatically "
863                      "determining what EPG data to set for a channel."),
864       .off      = offsetof(epggrab_channel_t, only_one),
865       .notify   = epggrab_channel_class_only_one_notify,
866       .group    = 1
867     },
868     {
869       .type     = PT_INT,
870       .islist   = 1,
871       .id       = "update",
872       .name     = N_("Channel update options"),
873       .desc     = N_("Options used when updating channels."),
874       .notify   = epggrab_channel_class_update_notify,
875       .list     = epggrab_channel_class_update_enum,
876       .get      = epggrab_channel_class_update_get,
877       .set      = epggrab_channel_class_update_set,
878       .rend     = epggrab_channel_class_update_rend,
879       .opts     = PO_ADVANCED
880     },
881     {
882       .type     = PT_STR,
883       .id       = "comment",
884       .name     = N_("Comment"),
885       .desc     = N_("Free-form text field, enter whatever you like."),
886       .off      = offsetof(epggrab_channel_t, comment),
887       .group    = 1
888     },
889     {}
890   }
891 };
892 
893 /*
894  *
895  */
896 void
epggrab_channel_init(void)897 epggrab_channel_init( void )
898 {
899   TAILQ_INIT(&epggrab_channel_entries);
900   idclass_register(&epggrab_channel_class);
901 }
902 
903 void
epggrab_channel_done(void)904 epggrab_channel_done( void )
905 {
906   assert(TAILQ_FIRST(&epggrab_channel_entries) == NULL);
907   SKEL_FREE(epggrab_channel_skel);
908 }
909