From 00902f46135ddbfa86047192652d4f14611f09c2 Mon Sep 17 00:00:00 2001 From: Adam Sutton Date: Tue, 14 Aug 2012 12:57:18 +0100 Subject: [PATCH] Add multi-lingual support to the EPG. Fixes #227. --- Makefile | 2 + src/config2.c | 16 + src/config2.h | 4 + src/dvr/dvr.h | 13 +- src/dvr/dvr_autorec.c | 10 +- src/dvr/dvr_db.c | 135 ++++---- src/dvr/dvr_rec.c | 30 +- src/dvr/mkmux.c | 28 +- src/epg.c | 232 ++++++++++---- src/epg.h | 61 +++- src/epgdb.c | 4 +- src/epggrab/module/eit.c | 78 +++-- src/epggrab/module/opentv.c | 19 +- src/epggrab/module/pyepg.c | 12 +- src/epggrab/module/xmltv.c | 58 +++- src/htsp.c | 35 +- src/htsstr.c | 8 +- src/htsstr.h | 2 +- src/lang_codes.c | 561 +++++++++++++++++++++++++++++++++ src/lang_codes.h | 38 +++ src/lang_str.c | 191 +++++++++++ src/lang_str.h | 55 ++++ src/webui/extjs.c | 53 ++-- src/webui/simpleui.c | 11 +- src/webui/static/app/config.js | 11 +- src/webui/webui.c | 2 +- 26 files changed, 1387 insertions(+), 282 deletions(-) create mode 100644 src/lang_codes.c create mode 100644 src/lang_codes.h create mode 100644 src/lang_str.c create mode 100644 src/lang_str.h diff --git a/Makefile b/Makefile index 2b4e10a5..2c21d321 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,8 @@ SRCS = src/main.c \ src/filebundle.c \ src/muxes.c \ src/config2.c \ + src/lang_codes.c \ + src/lang_str.c \ SRCS += src/epggrab/module.c\ src/epggrab/channel.c\ diff --git a/src/config2.c b/src/config2.c index 2b635c44..4193b3e9 100644 --- a/src/config2.c +++ b/src/config2.c @@ -43,6 +43,22 @@ htsmsg_t *config_get_all ( void ) return htsmsg_copy(config); } +const char *config_get_language ( void ) +{ + return htsmsg_get_str(config, "language"); +} + +int config_set_language ( const char *lang ) +{ + const char *c = config_get_language(); + if (!c || strcmp(c, lang)) { + if (c) htsmsg_delete_field(config, "language"); + htsmsg_add_str(config, "language", lang); + return 1; + } + return 0; +} + const char *config_get_muxconfpath ( void ) { return htsmsg_get_str(config, "muxconfpath"); diff --git a/src/config2.h b/src/config2.h index b9d7d9a2..cd68e306 100644 --- a/src/config2.h +++ b/src/config2.h @@ -32,4 +32,8 @@ const char *config_get_muxconfpath ( void ); int config_set_muxconfpath ( const char *str ) __attribute__((warn_unused_result)); +const char *config_get_language ( void ); +int config_set_language ( const char *str ) + __attribute__((warn_unused_result)); + #endif /* __TVH_CONFIG__H__ */ diff --git a/src/dvr/dvr.h b/src/dvr/dvr.h index 154f49dc..909f482f 100644 --- a/src/dvr/dvr.h +++ b/src/dvr/dvr.h @@ -24,6 +24,7 @@ #include "channels.h" #include "subscriptions.h" #include "muxer.h" +#include "lang_str.h" typedef struct dvr_config { char *dvr_config_name; @@ -129,10 +130,8 @@ typedef struct dvr_entry { char *de_creator; char *de_filename; /* Initially null if no filename has been generated yet */ - char *de_title; /* Title in UTF-8 (from EPG) */ - char *de_ititle; /* Internal title optionally with channelname - date and time pre/post/fixed */ - char *de_desc; /* Description in UTF-8 (from EPG) */ + lang_str_t *de_title; /* Title in UTF-8 (from EPG) */ + lang_str_t *de_desc; /* Description in UTF-8 (from EPG) */ epg_genre_t de_content_type; /* Content type (from EPG) */ dvr_prio_t de_pri; @@ -235,6 +234,8 @@ typedef struct dvr_autorec_entry { * Prototypes */ +void dvr_make_title(char *output, size_t outlen, dvr_entry_t *de); + dvr_config_t *dvr_config_find_by_name(const char *name); dvr_config_t *dvr_config_find_by_name_default(const char *name); @@ -266,7 +267,9 @@ dvr_entry_t *dvr_entry_create(const char *dvr_config_name, const char *creator, dvr_autorec_entry_t *dae, dvr_prio_t pri); -dvr_entry_t *dvr_entry_update(dvr_entry_t *de, const char* de_title, int de_start, int de_stop); +dvr_entry_t *dvr_entry_update + (dvr_entry_t *de, const char* de_title, const char *lang, + int de_start, int de_stop); void dvr_init(void); diff --git a/src/dvr/dvr_autorec.c b/src/dvr/dvr_autorec.c index ad9cb442..d439c273 100644 --- a/src/dvr/dvr_autorec.c +++ b/src/dvr/dvr_autorec.c @@ -89,9 +89,11 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e) if (!e->episode->brand || dae->dae_brand != e->episode->brand) return 0; if(dae->dae_title != NULL && dae->dae_title[0] != '\0') { - if(e->episode->title == NULL || - regexec(&dae->dae_title_preg, e->episode->title, 0, NULL, 0)) - return 0; + lang_str_ele_t *ls; + if(!e->episode->title) return 0; + RB_FOREACH(ls, e->episode->title, link) + if (!regexec(&dae->dae_title_preg, ls->str, 0, NULL, 0)) break; + if (!ls) return 0; } // Note: ignore channel test if we allow quality unlocking @@ -560,7 +562,7 @@ void dvr_autorec_add_series_link epg_episode_get_epnum(ee, &epnum); epnump = &epnum; } - _dvr_autorec_add(dvr_config_name, event->episode->title, + _dvr_autorec_add(dvr_config_name, NULL,/*TODO DVR lang_str event->episode->title,*/ cfg->dvr_sl_channel_lock ? event->channel : NULL, NULL, 0, // tag/content type cfg->dvr_sl_brand_lock ? ee->brand : NULL, diff --git a/src/dvr/dvr_db.c b/src/dvr/dvr_db.c index f23bb1b1..e5160357 100644 --- a/src/dvr/dvr_db.c +++ b/src/dvr/dvr_db.c @@ -153,7 +153,7 @@ dvr_entry_notify(dvr_entry_t *de) /** * */ -static void +void dvr_make_title(char *output, size_t outlen, dvr_entry_t *de) { struct tm tm; @@ -167,7 +167,7 @@ dvr_make_title(char *output, size_t outlen, dvr_entry_t *de) output[0] = 0; snprintf(output + strlen(output), outlen - strlen(output), - "%s", de->de_title); + "%s", lang_str_get(de->de_title, NULL)); localtime_r(&de->de_start, &tm); @@ -210,13 +210,8 @@ static void dvr_entry_link(dvr_entry_t *de) { time_t now, preamble; - char buf[100]; dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); - dvr_make_title(buf, sizeof(buf), de); - - de->de_ititle = strdup(buf); - de->de_refcnt = 1; LIST_INSERT_HEAD(&dvrentries, de, de_global_link); @@ -301,8 +296,22 @@ static dvr_entry_t *_dvr_entry_create ( de->de_stop_extra = cfg->dvr_extra_time_post; de->de_config_name = strdup(cfg->dvr_config_name); de->de_creator = strdup(creator); - de->de_title = strdup(title); - de->de_desc = description ? strdup(description) : NULL; + + de->de_desc = NULL; + if (e && e->episode) { + de->de_title = lang_str_copy(e->episode->title); + if (e->episode->description) + de->de_desc = lang_str_copy(e->episode->description); + else if (e->episode->summary) + de->de_desc = lang_str_copy(e->episode->summary); + } else if (title) { + de->de_title = lang_str_create(); + lang_str_add(de->de_title, title, NULL, 0); + if (description) { + de->de_desc = lang_str_create(); + lang_str_add(de->de_desc, description, NULL, 0); + } + } if (content_type) de->de_content_type = *content_type; de->de_bcast = e; if (e) e->getref((epg_object_t*)e); @@ -320,7 +329,7 @@ static dvr_entry_t *_dvr_entry_create ( tvhlog(LOG_INFO, "dvr", "\"%s\" on \"%s\" starting at %s, " "scheduled for recording by \"%s\"", - de->de_title, de->de_channel->ch_name, tbuf, creator); + lang_str_get(de->de_title, NULL), de->de_channel->ch_name, tbuf, creator); dvrdb_changed(); dvr_entry_save(de); @@ -362,9 +371,7 @@ dvr_entry_create_by_event(const char *config_name, return _dvr_entry_create(config_name, e, e->channel, e->start, e->stop, start_extra, stop_extra, - e->episode->title, - e->episode->description ? e->episode->description - : e->episode->summary, + NULL, NULL, LIST_FIRST(&e->episode->genre), creator, dae, pri); } @@ -416,9 +423,8 @@ dvr_entry_dec_ref(dvr_entry_t *de) free(de->de_config_name); free(de->de_creator); - free(de->de_title); - free(de->de_ititle); - free(de->de_desc); + if (de->de_title) lang_str_destroy(de->de_title); + if (de->de_desc) lang_str_destroy(de->de_desc); if(de->de_bcast) de->de_bcast->putref((epg_object_t*)de->de_bcast); free(de); @@ -456,11 +462,12 @@ static void dvr_db_load_one(htsmsg_t *c, int id) { dvr_entry_t *de; - const char *s, *title, *creator; + const char *s, *creator; channel_t *ch; uint32_t start, stop, bcid; int d; dvr_config_t *cfg; + lang_str_t *title, *ls; if(htsmsg_get_u32(c, "start", &start)) return; @@ -475,7 +482,7 @@ dvr_db_load_one(htsmsg_t *c, int id) s = htsmsg_get_str(c, "config_name"); cfg = dvr_config_find_by_name_default(s); - if((title = htsmsg_get_str(c, "title")) == NULL) + if(!(title = lang_str_deserialize(c, "title"))) return; if((creator = htsmsg_get_str(c, "creator")) == NULL) @@ -493,7 +500,7 @@ dvr_db_load_one(htsmsg_t *c, int id) de->de_stop = stop; de->de_config_name = strdup(cfg->dvr_config_name); de->de_creator = strdup(creator); - de->de_title = strdup(title); + de->de_title = title; de->de_pri = dvr_pri2val(htsmsg_get_str(c, "pri")); if(htsmsg_get_s32(c, "start_extra", &d)) @@ -513,7 +520,8 @@ dvr_db_load_one(htsmsg_t *c, int id) de->de_stop_extra = d; - tvh_str_set(&de->de_desc, htsmsg_get_str(c, "description")); + if ((ls = lang_str_deserialize(c, "description"))) + de->de_desc = ls; tvh_str_set(&de->de_filename, htsmsg_get_str(c, "filename")); htsmsg_get_u32(c, "errorcode", &de->de_last_error); @@ -592,10 +600,10 @@ dvr_entry_save(dvr_entry_t *de) if(de->de_filename != NULL) htsmsg_add_str(m, "filename", de->de_filename); - htsmsg_add_str(m, "title", de->de_title); + lang_str_serialize(de->de_title, m, "title"); if(de->de_desc != NULL) - htsmsg_add_str(m, "description", de->de_desc); + lang_str_serialize(de->de_desc, m, "description"); htsmsg_add_str(m, "pri", dvr_val2pri(de->de_pri)); @@ -635,64 +643,59 @@ dvr_timer_expire(void *aux) } static dvr_entry_t *_dvr_entry_update - ( dvr_entry_t *de, epg_broadcast_t *e, const char *de_title, - int de_start, int de_stop ) + ( dvr_entry_t *de, epg_broadcast_t *e, + const char *title, const char *lang, + int start, int stop ) { int save = 0; - const char *title; - int start, stop; + /* Start/Stop */ if (e) { - if (e->episode) - title = e->episode->title; - else - title = NULL; start = e->start; stop = e->stop; - } else { - title = de_title; - start = de_start; - stop = de_stop; } - - if (title && (!de->de_title || strcmp(de->de_title, title))) { - free(de->de_title); - de->de_title = strdup(title); - save = 1; - } - - if (stop != de->de_stop) { - de->de_stop = stop; - save = 1; - } - - if (start != de->de_start) { + if (start && (start != de->de_start)) { de->de_start = start; save = 1; } + if (stop && (stop != de->de_stop)) { + de->de_stop = stop; + save = 1; + } - if (e) { - epg_genre_t *g; - if (e->episode && (g = LIST_FIRST(&e->episode->genre))) { - if (g->code != de->de_content_type.code) { - de->de_content_type.code = g->code; - save = 1; - } - } - if (de->de_bcast != e) { - de->de_bcast->putref((epg_object_t*)de->de_bcast); - de->de_bcast = e; - e->getref((epg_object_t*)e); + /* Title */ + if (e && e->episode && e->episode->title) { + if (de->de_title) lang_str_destroy(de->de_title); + de->de_title = lang_str_copy(e->episode->title); + } else if (title) { + if (!de->de_title) de->de_title = lang_str_create(); + save = lang_str_add(de->de_title, title, lang, 1); + } + + /* Genre */ + if (e && e->episode) { + epg_genre_t *g = LIST_FIRST(&e->episode->genre); + if (g && (g->code != de->de_content_type.code)) { + de->de_content_type.code = g->code; save = 1; } } + /* Broadcast */ + if (e && (de->de_bcast != e)) { + de->de_bcast->putref(de->de_bcast); + de->de_bcast = e; + e->getref(e); + save = 1; + } + + /* Save changes */ if (save) { dvr_entry_save(de); htsp_dvr_entry_update(de); dvr_entry_notify(de); - - tvhlog(LOG_INFO, "dvr", "\"%s\" on \"%s\": Updated Timer", de->de_title, de->de_channel->ch_name); + tvhlog(LOG_INFO, "dvr", "\"%s\" on \"%s\": Updated Timer", + lang_str_get(de->de_title, NULL), de->de_channel->ch_name); } return de; @@ -702,9 +705,11 @@ static dvr_entry_t *_dvr_entry_update * */ dvr_entry_t * -dvr_entry_update(dvr_entry_t *de, const char* de_title, int de_start, int de_stop) +dvr_entry_update + (dvr_entry_t *de, const char* de_title, const char *lang, + int de_start, int de_stop) { - return _dvr_entry_update(de, NULL, de_title, de_start, de_stop); + return _dvr_entry_update(de, NULL, de_title, lang, de_start, de_stop); } /** @@ -725,7 +730,7 @@ dvr_event_replaced(epg_broadcast_t *e, epg_broadcast_t *new_e) if (ude == NULL && de->de_sched_state == DVR_SCHEDULED) dvr_entry_cancel(de); else if(new_e->episode && new_e->episode->title) - _dvr_entry_update(de, new_e, NULL, 0, 0); + _dvr_entry_update(de, new_e, NULL, NULL, 0, 0); } } @@ -733,7 +738,7 @@ void dvr_event_updated ( epg_broadcast_t *e ) { dvr_entry_t *de; de = dvr_entry_find_by_event(e); - if (de) _dvr_entry_update(de, e, NULL, 0, 0); + if (de) _dvr_entry_update(de, e, NULL, NULL, 0, 0); } /** diff --git a/src/dvr/dvr_rec.c b/src/dvr/dvr_rec.c index a5ea2cb6..8e492859 100755 --- a/src/dvr/dvr_rec.c +++ b/src/dvr/dvr_rec.c @@ -69,7 +69,7 @@ dvr_rec_subscribe(dvr_entry_t *de) else weight = 300; - snprintf(buf, sizeof(buf), "DVR: %s", de->de_title); + snprintf(buf, sizeof(buf), "DVR: %s", lang_str_get(de->de_title, NULL)); if(de->de_mc == MC_PASS) { streaming_queue_init(&de->de_sq, SMT_PACKET); @@ -196,11 +196,11 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss) char path[500]; int tally = 0; struct stat st; - char *filename; + char filename[1000]; struct tm tm; dvr_config_t *cfg = dvr_config_find_by_name_default(de->de_config_name); - filename = strdup(de->de_ititle); + dvr_make_title(filename, sizeof(filename), de); cleanupfilename(filename,cfg->dvr_flags); snprintf(path, sizeof(path), "%s", cfg->dvr_storage); @@ -232,7 +232,7 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss) if(cfg->dvr_flags & DVR_DIR_PER_TITLE) { - char *title = strdup(de->de_title); + char *title = strdup(lang_str_get(de->de_title, NULL)); cleanupfilename(title,cfg->dvr_flags); snprintf(path + strlen(path), sizeof(path) - strlen(path), "/%s", title); @@ -242,7 +242,6 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss) /* */ if(makedirs(path) != 0) { - free(filename); return -1; } @@ -270,7 +269,6 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss) tvh_str_set(&de->de_filename, fullname); - free(filename); return 0; } @@ -290,7 +288,7 @@ dvr_rec_fatal_error(dvr_entry_t *de, const char *fmt, ...) tvhlog(LOG_ERR, "dvr", "Recording error: \"%s\": %s", - de->de_filename ?: de->de_title, msgbuf); + de->de_filename ?: lang_str_get(de->de_title, NULL), msgbuf); } @@ -336,7 +334,7 @@ dvr_rec_start(dvr_entry_t *de, const streaming_start_t *ss) return; } - if(muxer_init(de->de_mux, ss, de->de_ititle)) { + if(muxer_init(de->de_mux, ss, lang_str_get(de->de_title, NULL))) { dvr_rec_fatal_error(de, "Unable to init file"); return; } @@ -352,7 +350,7 @@ dvr_rec_start(dvr_entry_t *de, const streaming_start_t *ss) "adapter: \"%s\", " "network: \"%s\", mux: \"%s\", provider: \"%s\", " "service: \"%s\"", - de->de_ititle, + de->de_filename ?: lang_str_get(de->de_title, NULL), si->si_adapter ?: "", si->si_network ?: "", si->si_mux ?: "", @@ -459,7 +457,7 @@ dvr_thread(void *aux) tvhlog(LOG_INFO, "dvr", "Recording completed: \"%s\"", - de->de_filename ?: de->de_title); + de->de_filename ?: lang_str_get(de->de_title, NULL)); } else { @@ -468,7 +466,7 @@ dvr_thread(void *aux) tvhlog(LOG_ERR, "dvr", "Recording stopped: \"%s\": %s", - de->de_filename ?: de->de_title, + de->de_filename ?: lang_str_get(de->de_title, NULL), streaming_code2txt(sm->sm_code)); } } @@ -494,7 +492,7 @@ dvr_thread(void *aux) dvr_rec_set_state(de, DVR_RS_ERROR, code); tvhlog(LOG_ERR, "dvr", "Streaming error: \"%s\": %s", - de->de_filename ?: de->de_title, + de->de_filename ?: lang_str_get(de->de_title, NULL), streaming_code2txt(code)); } } @@ -507,7 +505,7 @@ dvr_thread(void *aux) tvhlog(LOG_ERR, "dvr", "Recording unable to start: \"%s\": %s", - de->de_filename ?: de->de_title, + de->de_filename ?: lang_str_get(de->de_title, NULL), streaming_code2txt(sm->sm_code)); } break; @@ -531,7 +529,7 @@ dvr_thread(void *aux) static void dvr_spawn_postproc(dvr_entry_t *de, const char *dvr_postproc) { - char *fmap[256]; + const char *fmap[256]; char **args; char start[16]; char stop[16]; @@ -554,8 +552,8 @@ dvr_spawn_postproc(dvr_entry_t *de, const char *dvr_postproc) fmap['b'] = basename(fbasename); /* basename of recoding */ fmap['c'] = de->de_channel->ch_name; /* channel name */ fmap['C'] = de->de_creator; /* user who created this recording */ - fmap['t'] = de->de_title; /* program title */ - fmap['d'] = de->de_desc; /* program description */ + fmap['t'] = lang_str_get(de->de_title, NULL); /* program title */ + fmap['d'] = lang_str_get(de->de_desc, NULL); /* program description */ /* error message, empty if no error (FIXME:?) */ fmap['e'] = tvh_strdupa(streaming_code2txt(de->de_last_error)); fmap['S'] = start; /* start time, unix epoch */ diff --git a/src/dvr/mkmux.c b/src/dvr/mkmux.c index 133dcf82..93fa24fc 100644 --- a/src/dvr/mkmux.c +++ b/src/dvr/mkmux.c @@ -416,7 +416,7 @@ mk_write_segment_header(mk_mux_t *mkm, int64_t size) * */ static htsbuf_queue_t * -build_tag_string(const char *name, const char *value, +build_tag_string(const char *name, const char *value, const char *lang, int targettype, const char *targettypename) { htsbuf_queue_t *q = htsbuf_queue_alloc(0); @@ -432,7 +432,7 @@ build_tag_string(const char *name, const char *value, ebml_append_string(st, 0x45a3, name); ebml_append_string(st, 0x4487, value); ebml_append_uint(st, 0x4484, 1); - ebml_append_string(st, 0x447a, "und"); + ebml_append_string(st, 0x447a, lang ?: "und"); ebml_append_master(q, 0x67c8, st); return q; @@ -448,7 +448,7 @@ build_tag_int(const char *name, int value, { char str[64]; snprintf(str, sizeof(str), "%d", value); - return build_tag_string(name, str, targettype, targettypename); + return build_tag_string(name, str, NULL, targettype, targettypename); } @@ -475,6 +475,7 @@ _mk_build_metadata(const dvr_entry_t *de, const epg_broadcast_t *ebc) localtime_r(de ? &de->de_start : &ebc->start, &tm); epg_episode_t *ee = NULL; channel_t *ch; + lang_str_t *ls = NULL; if (ebc) ee = ebc->episode; else if (de->de_bcast) ee = de->de_bcast->episode; @@ -491,9 +492,9 @@ _mk_build_metadata(const dvr_entry_t *de, const epg_broadcast_t *ebc) tm.tm_min, tm.tm_sec); - addtag(q, build_tag_string("DATE_BROADCASTED", datestr, 0, NULL)); + addtag(q, build_tag_string("DATE_BROADCASTED", datestr, NULL, 0, NULL)); - addtag(q, build_tag_string("ORIGINAL_MEDIA_TYPE", "TV", 0, NULL)); + addtag(q, build_tag_string("ORIGINAL_MEDIA_TYPE", "TV", NULL, 0, NULL)); if(de && de->de_content_type.code) { eg = &de->de_content_type; @@ -501,17 +502,22 @@ _mk_build_metadata(const dvr_entry_t *de, const epg_broadcast_t *ebc) eg = LIST_FIRST(&ee->genre); } if(eg && epg_genre_get_str(eg, 1, 0, ctype, 100)) - addtag(q, build_tag_string("CONTENT_TYPE", ctype, 0, NULL)); + addtag(q, build_tag_string("CONTENT_TYPE", ctype, NULL, 0, NULL)); if(ch) - addtag(q, build_tag_string("TVCHANNEL", ch->ch_name, 0, NULL)); + addtag(q, build_tag_string("TVCHANNEL", ch->ch_name, NULL, 0, NULL)); if(de && de->de_desc) - addtag(q, build_tag_string("SUMMARY", de->de_desc, 0, NULL)); + ls = de->de_desc; else if (ee && ee->description) - addtag(q, build_tag_string("SUMMARY", ee->description, 0, NULL)); + ls = ee->description; else if (ee && ee->summary) - addtag(q, build_tag_string("SUMMARY", ee->summary, 0, NULL)); + ls = ee->summary; + if (ls) { + lang_str_ele_t *e; + RB_FOREACH(e, ls, link) + addtag(q, build_tag_string("SUMMARY", e->str, e->lang, 0, NULL)); + } if (ee) { epg_episode_num_t num; @@ -527,7 +533,7 @@ _mk_build_metadata(const dvr_entry_t *de, const epg_broadcast_t *ebc) 40, "PART")); if (num.text) addtag(q, build_tag_string("SYNOPSIS", - num.text, 0, NULL)); + num.text, NULL, 0, NULL)); } return q; diff --git a/src/epg.c b/src/epg.c index f15f56c0..2c554b44 100644 --- a/src/epg.c +++ b/src/epg.c @@ -272,7 +272,7 @@ static int _epg_object_set_str ( void *o, char **old, const char *new, epggrab_module_t *src ) { int save = 0; - epg_object_t *eo = (epg_object_t*)o; + epg_object_t *eo = o; if ( !eo || !new ) return 0; if ( !_epg_object_set_grabber(eo, src) && *old ) return 0; if ( !*old || strcmp(*old, new) ) { @@ -284,6 +284,21 @@ static int _epg_object_set_str return save; } +static int _epg_object_set_lang_str + ( void *o, lang_str_t **old, const char *newstr, const char *newlang, + epggrab_module_t *src ) +{ + int update, save; + epg_object_t *eo = o; + if ( !eo || !newstr ) return 0; + update = _epg_object_set_grabber(eo, src); + if (!*old) *old = lang_str_create(); + save = lang_str_add(*old, newstr, newlang, update); + if (save) + _epg_object_set_updated(eo); + return save; +} + static int _epg_object_set_u8 ( void *o, uint8_t *old, const uint8_t new, epggrab_module_t *src ) { @@ -326,8 +341,8 @@ static void _epg_brand_destroy ( void *eo ) assert(0); } _epg_object_destroy(eo, &epg_brands); - if (eb->title) free(eb->title); - if (eb->summary) free(eb->summary); + if (eb->title) lang_str_destroy(eb->title); + if (eb->summary) lang_str_destroy(eb->summary); if (eb->image) free(eb->image); free(eb); } @@ -364,17 +379,19 @@ epg_brand_t *epg_brand_find_by_id ( uint64_t id ) } int epg_brand_set_title - ( epg_brand_t *brand, const char *title, epggrab_module_t *src ) + ( epg_brand_t *brand, const char *title, const char *lang, + epggrab_module_t *src ) { - if (!brand || !title) return 0; - return _epg_object_set_str(brand, &brand->title, title, src); + if (!brand || !title || !*title) return 0; + return _epg_object_set_lang_str(brand, &brand->title, title, lang, src); } int epg_brand_set_summary - ( epg_brand_t *brand, const char *summary, epggrab_module_t *src ) + ( epg_brand_t *brand, const char *summary, const char *lang, + epggrab_module_t *src ) { - if (!brand || !summary) return 0; - return _epg_object_set_str(brand, &brand->summary, summary, src); + if (!brand || !summary || !*summary) return 0; + return _epg_object_set_lang_str(brand, &brand->summary, summary, lang, src); } int epg_brand_set_image @@ -429,9 +446,9 @@ htsmsg_t *epg_brand_serialize ( epg_brand_t *brand ) if ( !brand || !brand->uri ) return NULL; if ( !(m = _epg_object_serialize(brand)) ) return NULL; if (brand->title) - htsmsg_add_str(m, "title", brand->title); + lang_str_serialize(brand->title, m, "title"); if (brand->summary) - htsmsg_add_str(m, "summary", brand->summary); + lang_str_serialize(brand->summary, m, "summary"); if (brand->season_count) htsmsg_add_u32(m, "season-count", brand->season_count); return m; @@ -442,15 +459,22 @@ epg_brand_t *epg_brand_deserialize ( htsmsg_t *m, int create, int *save ) epg_object_t **skel = _epg_brand_skel(); epg_brand_t *eb; uint32_t u32; - const char *str; + lang_str_t *ls; + lang_str_ele_t *e; if ( !_epg_object_deserialize(m, *skel) ) return NULL; if ( !(eb = epg_brand_find_by_uri((*skel)->uri, create, save)) ) return NULL; - if ( (str = htsmsg_get_str(m, "title")) ) - *save |= epg_brand_set_title(eb, str, NULL); - if ( (str = htsmsg_get_str(m, "summary")) ) - *save |= epg_brand_set_summary(eb, str, NULL); + if ((ls = lang_str_deserialize(m, "title"))) { + RB_FOREACH(e, ls, link) + *save |= epg_brand_set_title(eb, e->str, e->lang, NULL); + lang_str_destroy(ls); + } + if ((ls = lang_str_deserialize(m, "summary"))) { + RB_FOREACH(e, ls, link) + *save |= epg_brand_set_summary(eb, e->str, e->lang, NULL); + lang_str_destroy(ls); + } if ( !htsmsg_get_u32(m, "season-count", &u32) ) *save |= epg_brand_set_season_count(eb, u32, NULL); @@ -470,6 +494,18 @@ htsmsg_t *epg_brand_list ( void ) return a; } +const char *epg_brand_get_title ( const epg_brand_t *b, const char *lang ) +{ + if (!b || !b->title) return NULL; + return lang_str_get(b->title, lang); +} + +const char *epg_brand_get_summary ( const epg_brand_t *b, const char *lang ) +{ + if (!b || !b->summary) return NULL; + return lang_str_get(b->summary, lang); +} + /* ************************************************************************** * Season * *************************************************************************/ @@ -482,8 +518,8 @@ static void _epg_season_destroy ( void *eo ) assert(0); } _epg_object_destroy(eo, &epg_seasons); - if (es->brand) _epg_brand_rem_season(es->brand, es); - if (es->summary) free(es->summary); + if (es->brand) _epg_brand_rem_season(es->brand, es); + if (es->summary) lang_str_destroy(es->summary); if (es->image) free(es->image); free(es); } @@ -520,10 +556,11 @@ epg_season_t *epg_season_find_by_id ( uint64_t id ) } int epg_season_set_summary - ( epg_season_t *season, const char *summary, epggrab_module_t *src ) + ( epg_season_t *season, const char *summary, const char *lang, + epggrab_module_t *src ) { - if (!season || !summary) return 0; - return _epg_object_set_str(season, &season->summary, summary, src); + if (!season || !summary || !*summary) return 0; + return _epg_object_set_lang_str(season, &season->summary, summary, lang, src); } int epg_season_set_image @@ -585,7 +622,7 @@ htsmsg_t *epg_season_serialize ( epg_season_t *season ) if (!season || !season->uri) return NULL; if (!(m = _epg_object_serialize((epg_object_t*)season))) return NULL; if (season->summary) - htsmsg_add_str(m, "summary", season->summary); + lang_str_serialize(season->summary, m, "summary"); if (season->number) htsmsg_add_u32(m, "number", season->number); if (season->episode_count) @@ -602,12 +639,18 @@ epg_season_t *epg_season_deserialize ( htsmsg_t *m, int create, int *save ) epg_brand_t *eb; uint32_t u32; const char *str; + lang_str_t *ls; + lang_str_ele_t *e; if ( !_epg_object_deserialize(m, *skel) ) return NULL; if ( !(es = epg_season_find_by_uri((*skel)->uri, create, save)) ) return NULL; - if ( (str = htsmsg_get_str(m, "summary")) ) - *save |= epg_season_set_summary(es, str, NULL); + if ((ls = lang_str_deserialize(m, "summary"))) { + RB_FOREACH(e, ls, link) { + *save |= epg_season_set_summary(es, e->str, e->lang, NULL); + } + lang_str_destroy(ls); + } if ( !htsmsg_get_u32(m, "number", &u32) ) *save |= epg_season_set_number(es, u32, NULL); if ( !htsmsg_get_u32(m, "episode-count", &u32) ) @@ -620,6 +663,13 @@ epg_season_t *epg_season_deserialize ( htsmsg_t *m, int create, int *save ) return es; } +const char *epg_season_get_summary + ( const epg_season_t *s, const char *lang ) +{ + if (!s || !s->summary) return NULL; + return lang_str_get(s->summary, lang); +} + /* ************************************************************************** * Episode * *************************************************************************/ @@ -684,10 +734,10 @@ static void _epg_episode_destroy ( void *eo ) _epg_object_destroy(eo, &epg_episodes); if (ee->brand) _epg_brand_rem_episode(ee->brand, ee); if (ee->season) _epg_season_rem_episode(ee->season, ee); - if (ee->title) free(ee->title); - if (ee->subtitle) free(ee->subtitle); - if (ee->summary) free(ee->summary); - if (ee->description) free(ee->description); + if (ee->title) lang_str_destroy(ee->title); + if (ee->subtitle) lang_str_destroy(ee->subtitle); + if (ee->summary) lang_str_destroy(ee->summary); + if (ee->description) lang_str_destroy(ee->description); while ((g = LIST_FIRST(&ee->genre))) { LIST_REMOVE(g, link); free(g); @@ -728,31 +778,38 @@ epg_episode_t *epg_episode_find_by_id ( uint64_t id ) } int epg_episode_set_title - ( epg_episode_t *episode, const char *title, epggrab_module_t *src ) + ( epg_episode_t *episode, const char *title, const char *lang, + epggrab_module_t *src ) { - if (!episode || !title) return 0; - return _epg_object_set_str(episode, &episode->title, title, src); + if (!episode || !title || !*title) return 0; + return _epg_object_set_lang_str(episode, &episode->title, title, lang, src); } int epg_episode_set_subtitle - ( epg_episode_t *episode, const char *subtitle, epggrab_module_t *src ) + ( epg_episode_t *episode, const char *subtitle, const char *lang, + epggrab_module_t *src ) { - if (!episode || !subtitle) return 0; - return _epg_object_set_str(episode, &episode->subtitle, subtitle, src); + if (!episode || !subtitle || !*subtitle) return 0; + return _epg_object_set_lang_str(episode, &episode->subtitle, + subtitle, lang, src); } int epg_episode_set_summary - ( epg_episode_t *episode, const char *summary, epggrab_module_t *src ) + ( epg_episode_t *episode, const char *summary, const char *lang, + epggrab_module_t *src ) { - if (!episode || !summary) return 0; - return _epg_object_set_str(episode, &episode->summary, summary, src); + if (!episode || !summary || !*summary) return 0; + return _epg_object_set_lang_str(episode, &episode->summary, + summary, lang, src); } int epg_episode_set_description - ( epg_episode_t *episode, const char *desc, epggrab_module_t *src ) + ( epg_episode_t *episode, const char *desc, const char *lang, + epggrab_module_t *src ) { - if (!episode || !desc) return 0; - return _epg_object_set_str(episode, &episode->description, desc, src); + if (!episode || !desc || !*desc) return 0; + return _epg_object_set_lang_str(episode, &episode->description, + desc, lang, src); } int epg_episode_set_image @@ -948,6 +1005,7 @@ int epg_episode_number_cmp ( epg_episode_num_t *a, epg_episode_num_t *b ) // WIBNI: this could do with soem proper matching, maybe some form of // fuzzy string match. I did try a few things, but none of them // were very reliable. +#if TODO_FUZZY_MATCH int epg_episode_fuzzy_match ( epg_episode_t *episode, const char *uri, const char *title, const char *summary, const char *description ) @@ -957,6 +1015,7 @@ int epg_episode_fuzzy_match if ( title && episode->title && (strstr(title, episode->title) || strstr(episode->title, title)) ) return 1; return 0; } +#endif htsmsg_t *epg_episode_serialize ( epg_episode_t *episode ) { @@ -965,13 +1024,13 @@ htsmsg_t *epg_episode_serialize ( epg_episode_t *episode ) if (!episode || !episode->uri) return NULL; if (!(m = _epg_object_serialize((epg_object_t*)episode))) return NULL; if (episode->title) - htsmsg_add_str(m, "title", episode->title); + lang_str_serialize(episode->title, m, "title"); if (episode->subtitle) - htsmsg_add_str(m, "subtitle", episode->subtitle); + lang_str_serialize(episode->subtitle, m, "subtitle"); if (episode->summary) - htsmsg_add_str(m, "summary", episode->summary); + lang_str_serialize(episode->summary, m, "summary"); if (episode->description) - htsmsg_add_str(m, "description", episode->description); + lang_str_serialize(episode->description, m, "description"); htsmsg_add_msg(m, "epnum", epg_episode_num_serialize(&episode->epnum)); LIST_FOREACH(eg, &episode->genre, link) { if (!a) a = htsmsg_create_list(); @@ -998,18 +1057,33 @@ epg_episode_t *epg_episode_deserialize ( htsmsg_t *m, int create, int *save ) htsmsg_t *sub; htsmsg_field_t *f; uint32_t u32; + lang_str_t *ls; + lang_str_ele_t *e; if ( !_epg_object_deserialize(m, *skel) ) return NULL; - if ( !(ee = epg_episode_find_by_uri((*skel)->uri, create, save)) ) return NULL; + if ( !(ee = epg_episode_find_by_uri((*skel)->uri, create, save)) ) + return NULL; - if ( (str = htsmsg_get_str(m, "title")) ) - *save |= epg_episode_set_title(ee, str, NULL); - if ( (str = htsmsg_get_str(m, "subtitle")) ) - *save |= epg_episode_set_subtitle(ee, str, NULL); - if ( (str = htsmsg_get_str(m, "summary")) ) - *save |= epg_episode_set_summary(ee, str, NULL); - if ( (str = htsmsg_get_str(m, "description")) ) - *save |= epg_episode_set_description(ee, str, NULL); + if ((ls = lang_str_deserialize(m, "title"))) { + RB_FOREACH(e, ls, link) + *save |= epg_episode_set_title(ee, e->str, e->lang, NULL); + lang_str_destroy(ls); + } + if ((ls = lang_str_deserialize(m, "subtitle"))) { + RB_FOREACH(e, ls, link) + *save |= epg_episode_set_subtitle(ee, e->str, e->lang, NULL); + lang_str_destroy(ls); + } + if ((ls = lang_str_deserialize(m, "summary"))) { + RB_FOREACH(e, ls, link) + *save |= epg_episode_set_summary(ee, e->str, e->lang, NULL); + lang_str_destroy(ls); + } + if ((ls = lang_str_deserialize(m, "description"))) { + RB_FOREACH(e, ls, link) + *save |= epg_episode_set_description(ee, e->str, e->lang, NULL); + lang_str_destroy(ls); + } if ( (sub = htsmsg_get_map(m, "epnum")) ) { epg_episode_num_deserialize(sub, &num); *save |= epg_episode_set_epnum(ee, &num, NULL); @@ -1039,6 +1113,34 @@ epg_episode_t *epg_episode_deserialize ( htsmsg_t *m, int create, int *save ) return ee; } +const char *epg_episode_get_title + ( const epg_episode_t *e, const char *lang ) +{ + if (!e || !e->title) return NULL; + return lang_str_get(e->title, lang); +} + +const char *epg_episode_get_subtitle + ( const epg_episode_t *e, const char *lang ) +{ + if (!e || !e->subtitle) return NULL; + return lang_str_get(e->subtitle, lang); +} + +const char *epg_episode_get_summary + ( const epg_episode_t *e, const char *lang ) +{ + if (!e || !e->summary) return NULL; + return lang_str_get(e->summary, lang); +} + +const char *epg_episode_get_description + ( const epg_episode_t *e, const char *lang ) +{ + if (!e || !e->description) return NULL; + return lang_str_get(e->description, lang); +} + /* ************************************************************************** * Channel * *************************************************************************/ @@ -1757,13 +1859,15 @@ htsmsg_t *epg_genres_list_all ( int major_only, int major_prefix ) static void _eqr_add ( epg_query_result_t *eqr, epg_broadcast_t *e, - epg_genre_t *genre, regex_t *preg, time_t start ) + epg_genre_t *genre, regex_t *preg, time_t start, const char *lang ) { + const char *title; + /* Ignore */ if ( e->stop < start ) return; - if ( !e->episode->title ) return; + if ( !(title = epg_episode_get_title(e->episode, lang)) ) return; if ( genre && !epg_genre_list_contains(&e->episode->genre, genre, 1) ) return; - if ( preg && regexec(preg, e->episode->title, 0, NULL, 0) ) return; + if ( preg && regexec(preg, title, 0, NULL, 0)) return; /* More space */ if ( eqr->eqr_entries == eqr->eqr_alloced ) { @@ -1778,17 +1882,17 @@ static void _eqr_add static void _eqr_add_channel ( epg_query_result_t *eqr, channel_t *ch, epg_genre_t *genre, - regex_t *preg, time_t start ) + regex_t *preg, time_t start, const char *lang ) { epg_broadcast_t *ebc; RB_FOREACH(ebc, &ch->ch_epg_schedule, sched_link) { - if ( ebc->episode ) _eqr_add(eqr, ebc, genre, preg, start); + if ( ebc->episode ) _eqr_add(eqr, ebc, genre, preg, start, lang); } } void epg_query0 ( epg_query_result_t *eqr, channel_t *channel, channel_tag_t *tag, - epg_genre_t *genre, const char *title ) + epg_genre_t *genre, const char *title, const char *lang ) { time_t now; channel_tag_mapping_t *ctm; @@ -1809,19 +1913,19 @@ void epg_query0 /* Single channel */ if (channel && !tag) { - _eqr_add_channel(eqr, channel, genre, preg, now); + _eqr_add_channel(eqr, channel, genre, preg, now, lang); /* Tag based */ } else if ( tag ) { LIST_FOREACH(ctm, &tag->ct_ctms, ctm_tag_link) { if(channel == NULL || ctm->ctm_channel == channel) - _eqr_add_channel(eqr, ctm->ctm_channel, genre, preg, now); + _eqr_add_channel(eqr, ctm->ctm_channel, genre, preg, now, lang); } /* All channels */ } else { RB_FOREACH(channel, &channel_name_tree, ch_name_link) { - _eqr_add_channel(eqr, channel, genre, preg, now); + _eqr_add_channel(eqr, channel, genre, preg, now, lang); } } if (preg) regfree(preg); @@ -1830,11 +1934,11 @@ void epg_query0 } void epg_query(epg_query_result_t *eqr, const char *channel, const char *tag, - epg_genre_t *genre, const char *title) + epg_genre_t *genre, const char *title, const char *lang) { channel_t *ch = channel ? channel_find_by_name(channel, 0, 0) : NULL; channel_tag_t *ct = tag ? channel_tag_find_by_name(tag, 0) : NULL; - epg_query0(eqr, ch, ct, genre, title); + epg_query0(eqr, ch, ct, genre, title, lang); } void epg_query_free(epg_query_result_t *eqr) diff --git a/src/epg.h b/src/epg.h index 97dcd7a7..336a0205 100644 --- a/src/epg.h +++ b/src/epg.h @@ -20,6 +20,7 @@ #define EPG_H #include "settings.h" +#include "lang_str.h" /* * External forward decls @@ -136,8 +137,8 @@ struct epg_brand { epg_object_t; ///< Base object - char *title; ///< Brand name - char *summary; ///< Brand summary + lang_str_t *title; ///< Brand name + lang_str_t *summary; ///< Brand summary uint16_t season_count; ///< Total number of seasons char *image; ///< Brand image @@ -150,12 +151,20 @@ epg_brand_t *epg_brand_find_by_uri ( const char *uri, int create, int *save ); epg_brand_t *epg_brand_find_by_id ( uint64_t id ); +/* Accessors */ +const char *epg_brand_get_title + ( const epg_brand_t *b, const char *lang ); +const char *epg_brand_get_summary + ( const epg_brand_t *b, const char *lang ); + /* Mutators */ int epg_brand_set_title - ( epg_brand_t *b, const char *title, struct epggrab_module *src ) + ( epg_brand_t *b, const char *title, const char *lang, + struct epggrab_module *src ) __attribute__((warn_unused_result)); int epg_brand_set_summary - ( epg_brand_t *b, const char *summary, struct epggrab_module *src ) + ( epg_brand_t *b, const char *summary, const char *lang, + struct epggrab_module *src ) __attribute__((warn_unused_result)); int epg_brand_set_season_count ( epg_brand_t *b, uint16_t season_count, struct epggrab_module *src ) @@ -180,7 +189,7 @@ struct epg_season { epg_object_t; ///< Parent object - char *summary; ///< Season summary + lang_str_t *summary; ///< Season summary uint16_t number; ///< The season number uint16_t episode_count; ///< Total number of episodes char *image; ///< Season image @@ -196,9 +205,14 @@ epg_season_t *epg_season_find_by_uri ( const char *uri, int create, int *save ); epg_season_t *epg_season_find_by_id ( uint64_t id ); +/* Accessors */ +const char *epg_season_get_summary + ( const epg_season_t *s, const char *lang ); + /* Mutators */ int epg_season_set_summary - ( epg_season_t *s, const char *summary, struct epggrab_module *src ) + ( epg_season_t *s, const char *summary, const char *lang, + struct epggrab_module *src ) __attribute__((warn_unused_result)); int epg_season_set_number ( epg_season_t *s, uint16_t number, struct epggrab_module *src ) @@ -240,10 +254,10 @@ struct epg_episode { epg_object_t; ///< Parent object - char *title; ///< Title - char *subtitle; ///< Sub-title - char *summary; ///< Summary - char *description; ///< An extended description + lang_str_t *title; ///< Title + lang_str_t *subtitle; ///< Sub-title + lang_str_t *summary; ///< Summary + lang_str_t *description; ///< An extended description char *image; ///< Episode image epg_genre_list_t genre; ///< Episode genre(s) epg_episode_num_t epnum; ///< Episode numbering @@ -265,18 +279,32 @@ epg_episode_t *epg_episode_find_by_uri ( const char *uri, int create, int *save ); epg_episode_t *epg_episode_find_by_id ( uint64_t id ); +/* Accessors */ +const char *epg_episode_get_title + ( const epg_episode_t *e, const char *lang ); +const char *epg_episode_get_subtitle + ( const epg_episode_t *e, const char *lang ); +const char *epg_episode_get_summary + ( const epg_episode_t *e, const char *lang ); +const char *epg_episode_get_description + ( const epg_episode_t *e, const char *lang ); + /* Mutators */ int epg_episode_set_title - ( epg_episode_t *e, const char *title, struct epggrab_module *src ) + ( epg_episode_t *e, const char *title, const char *lang, + struct epggrab_module *src ) __attribute__((warn_unused_result)); int epg_episode_set_subtitle - ( epg_episode_t *e, const char *subtitle, struct epggrab_module *src ) + ( epg_episode_t *e, const char *subtitle, const char *lang, + struct epggrab_module *src ) __attribute__((warn_unused_result)); int epg_episode_set_summary - ( epg_episode_t *e, const char *summary, struct epggrab_module *src ) + ( epg_episode_t *e, const char *summary, const char *lang, + struct epggrab_module *src ) __attribute__((warn_unused_result)); int epg_episode_set_description - ( epg_episode_t *e, const char *description, struct epggrab_module *src ) + ( epg_episode_t *e, const char *description, const char *lang, + struct epggrab_module *src ) __attribute__((warn_unused_result)); int epg_episode_set_number ( epg_episode_t *e, uint16_t number, struct epggrab_module *src ) @@ -444,9 +472,10 @@ void epg_query_sort(epg_query_result_t *eqr); /* Query routines */ void epg_query0(epg_query_result_t *eqr, struct channel *ch, - struct channel_tag *ct, epg_genre_t *genre, const char *title); + struct channel_tag *ct, epg_genre_t *genre, const char *title, + const char *lang); void epg_query(epg_query_result_t *eqr, const char *channel, const char *tag, - epg_genre_t *genre, const char *title); + epg_genre_t *genre, const char *title, const char *lang); /* ************************************************************************ diff --git a/src/epgdb.c b/src/epgdb.c index 9d48712d..19433c0a 100644 --- a/src/epgdb.c +++ b/src/epgdb.c @@ -77,9 +77,9 @@ static void _epgdb_v1_process ( htsmsg_t *c, epggrab_stats_t *stats ) if (!ee) return; if (save) stats->episodes.total++; if (title) - save |= epg_episode_set_title(ee, title, NULL); + save |= epg_episode_set_title(ee, title, NULL, NULL); if (desc) - save |= epg_episode_set_summary(ee, desc, NULL); + save |= epg_episode_set_summary(ee, desc, NULL, NULL); if (!htsmsg_get_u32(c, "episode", &u32)) save |= epg_episode_set_number(ee, u32, NULL); if (!htsmsg_get_u32(c, "part", &u32)) diff --git a/src/epggrab/module/eit.c b/src/epggrab/module/eit.c index 8ca9268e..ff48b973 100644 --- a/src/epggrab/module/eit.c +++ b/src/epggrab/module/eit.c @@ -93,10 +93,10 @@ typedef struct eit_event { char uri[257]; char suri[257]; - - char title[257]; - char summary[257]; - char desc[2000]; + + lang_str_t *title; + lang_str_t *summary; + lang_str_t *desc; char *default_charset; @@ -169,26 +169,38 @@ static int _eit_desc_short_event ( epggrab_module_t *mod, uint8_t *ptr, int len, eit_event_t *ev ) { int r; + char lang[4]; + char buf[256]; if ( len < 5 ) return -1; - /* Language (skip) */ + /* Language */ + memcpy(lang, ptr, 3); + lang[3] = '\0'; len -= 3; ptr += 3; /* Title */ - if ( (r = _eit_get_string_with_len(mod, ev->title, sizeof(ev->title), - ptr, len, ev->default_charset)) < 0 ) + if ( (r = _eit_get_string_with_len(mod, buf, sizeof(buf), + ptr, len, ev->default_charset)) < 0 ) { return -1; + } else { + if (!ev->title) ev->title = lang_str_create(); + lang_str_add(ev->title, buf, lang, 0); + } len -= r; ptr += r; if ( len < 1 ) return -1; /* Summary */ - if ( (r = _eit_get_string_with_len(mod, ev->summary, sizeof(ev->summary), - ptr, len, ev->default_charset)) < 0 ) + if ( (r = _eit_get_string_with_len(mod, buf, sizeof(buf), + ptr, len, ev->default_charset)) < 0 ) { return -1; + } else { + if (!ev->summary) ev->summary = lang_str_create(); + lang_str_add(ev->summary, buf, lang, 0); + } return 0; } @@ -201,6 +213,7 @@ static int _eit_desc_ext_event { int r, nitem; char ikey[256], ival[256]; + char buf[256], lang[4]; if (len < 6) return -1; @@ -208,7 +221,9 @@ static int _eit_desc_ext_event len -= 1; ptr += 1; - /* Language (skip) */ + /* Language */ + memcpy(lang, ptr, 3); + lang[3] = '\0'; len -= 3; ptr += 3; @@ -248,11 +263,14 @@ static int _eit_desc_ext_event /* Description */ if ( _eit_get_string_with_len(mod, - ev->desc + strlen(ev->desc), - sizeof(ev->desc) - strlen(ev->desc), + buf, sizeof(buf), ptr, len, - ev->default_charset) < 0 ) + ev->default_charset) < 0 ) { return -1; + } else { + if (!ev->desc) ev->desc = lang_str_create(); + lang_str_append(ev->desc, buf, lang); + } return 0; } @@ -394,6 +412,7 @@ static int _eit_process_event epg_episode_t *ee; epg_season_t *es; eit_event_t ev; + lang_str_ele_t *ls; if ( len < 12 ) return -1; @@ -432,7 +451,7 @@ static int _eit_process_event dllen -= 2; ptr += 2; - if (dllen < dlen) return ret; + if (dllen < dlen) break; switch (dtag) { case DVB_DESC_SHORT_EVENT: @@ -461,7 +480,7 @@ static int _eit_process_event break; } - if (r < 0) return ret; + if (r < 0) break; dllen -= dlen; ptr += dlen; } @@ -478,7 +497,9 @@ static int _eit_process_event ee = epg_episode_find_by_uri(ev.uri, 1, save); } else if ( !(ee = ebc->episode) ) { char *uri; - uri = epg_hash(ev.title, ev.summary, ev.desc); + uri = epg_hash(lang_str_get(ev.title, NULL), + lang_str_get(ev.summary, NULL), + lang_str_get(ev.desc, NULL)); if (uri) { ee = epg_episode_find_by_uri(uri, 1, save); free(uri); @@ -491,12 +512,18 @@ static int _eit_process_event /* Update Episode */ if (ee) { *save |= epg_episode_set_is_bw(ee, ev.bw, mod); - if ( *ev.title ) - *save |= epg_episode_set_title(ee, ev.title, mod); - if ( *ev.summary ) - *save |= epg_episode_set_summary(ee, ev.summary, mod); - if ( *ev.desc ) - *save |= epg_episode_set_description(ee, ev.desc, mod); + if ( ev.title ) { + RB_FOREACH(ls, ev.title, link) + *save |= epg_episode_set_title(ee, ls->str, ls->lang, mod); + } + if ( ev.summary ) { + RB_FOREACH(ls, ev.summary, link) + *save |= epg_episode_set_summary(ee, ls->str, ls->lang, mod); + } + if ( ev.desc ) { + RB_FOREACH(ls, ev.desc, link) + *save |= epg_episode_set_description(ee, ls->str, ls->lang, mod); + } if ( ev.genre ) *save |= epg_episode_set_genre(ee, ev.genre, mod); #if TODO_ADD_EXTRA @@ -514,9 +541,12 @@ static int _eit_process_event /* Tidy up */ #if TODO_ADD_EXTRA - if (ev.extra) htsmsg_destroy(ev.extra); + if (ev.extra) htsmsg_destroy(ev.extra); #endif - if (ev.genre) epg_genre_list_destroy(ev.genre); + if (ev.genre) epg_genre_list_destroy(ev.genre); + if (ev.title) lang_str_destroy(ev.title); + if (ev.summary) lang_str_destroy(ev.summary); + if (ev.desc) lang_str_destroy(ev.desc); return ret; } diff --git a/src/epggrab/module/opentv.c b/src/epggrab/module/opentv.c index 65cc38c5..d98a2b05 100644 --- a/src/epggrab/module/opentv.c +++ b/src/epggrab/module/opentv.c @@ -355,6 +355,12 @@ static int _opentv_parse_event_section epg_season_t *es; opentv_event_t ev; epggrab_module_t *src = (epggrab_module_t*)mod; + const char *lang = NULL; + const char *str; + + /* Get language (bit of a hack) */ + if (!strcmp(mod->dict->id, "skyit")) lang = "it"; + else if (!strcmp(mod->dict->id, "skyeng")) lang = "eng"; /* Channel */ cid = ((int)buf[0] << 8) | buf[1]; @@ -398,14 +404,15 @@ static int _opentv_parse_event_section /* Update */ if (ee) { - if (!ev.title && ebc->episode) - save |= epg_episode_set_title(ee, ebc->episode->title, src); - else if (ev.title) - save |= epg_episode_set_title(ee, ev.title, src); + if (!ev.title && ebc->episode) { + if ((str = epg_episode_get_title(ebc->episode, NULL))) + save |= epg_episode_set_title(ee, str, lang, NULL); + } else if (ev.title) + save |= epg_episode_set_title(ee, ev.title, lang, src); if (ev.summary) - save |= epg_episode_set_summary(ee, ev.summary, src); + save |= epg_episode_set_summary(ee, ev.summary, lang, src); if (ev.desc) - save |= epg_episode_set_description(ee, ev.desc, src); + save |= epg_episode_set_description(ee, ev.desc, lang, src); if (ev.cat) { epg_genre_list_t *egl = calloc(1, sizeof(epg_genre_list_t)); epg_genre_list_add_by_eit(egl, ev.cat); diff --git a/src/epggrab/module/pyepg.c b/src/epggrab/module/pyepg.c index 42901e9f..a9ccd0ee 100644 --- a/src/epggrab/module/pyepg.c +++ b/src/epggrab/module/pyepg.c @@ -127,12 +127,12 @@ static int _pyepg_parse_brand /* Set title */ if ((str = htsmsg_xml_get_cdata_str(tags, "title"))) { - save |= epg_brand_set_title(brand, str, mod); + save |= epg_brand_set_title(brand, str, NULL, mod); } /* Set summary */ if ((str = htsmsg_xml_get_cdata_str(tags, "summary"))) { - save |= epg_brand_set_summary(brand, str, mod); + save |= epg_brand_set_summary(brand, str, NULL, mod); } /* Set image */ @@ -182,7 +182,7 @@ static int _pyepg_parse_season /* Set summary */ if ((str = htsmsg_xml_get_cdata_str(tags, "summary"))) { - save |= epg_season_set_summary(season, str, mod); + save |= epg_season_set_summary(season, str, NULL, mod); } /* Set image */ @@ -246,15 +246,15 @@ static int _pyepg_parse_episode /* Set title/subtitle */ if ((str = htsmsg_xml_get_cdata_str(tags, "title"))) { - save |= epg_episode_set_title(episode, str, mod); + save |= epg_episode_set_title(episode, str, NULL, mod); } if ((str = htsmsg_xml_get_cdata_str(tags, "subtitle"))) { - save |= epg_episode_set_subtitle(episode, str, mod); + save |= epg_episode_set_subtitle(episode, str, NULL, mod); } /* Set summary */ if ((str = htsmsg_xml_get_cdata_str(tags, "summary"))) { - save |= epg_episode_set_summary(episode, str, mod); + save |= epg_episode_set_summary(episode, str, NULL, mod); } /* Number */ diff --git a/src/epggrab/module/xmltv.c b/src/epggrab/module/xmltv.c index d2d7b62b..32a14817 100644 --- a/src/epggrab/module/xmltv.c +++ b/src/epggrab/module/xmltv.c @@ -34,6 +34,7 @@ #include "spawn.h" #include "htsstr.h" +#include "lang_str.h" #include "epg.h" #include "epggrab.h" #include "epggrab/private.h" @@ -323,6 +324,27 @@ static epg_genre_list_t return egl; } +/* + * Parse a series of language strings + */ +static void +_xmltv_parse_lang_str ( lang_str_t **ls, htsmsg_t *tags, const char *tname ) +{ + htsmsg_t *e, *attrib; + htsmsg_field_t *f; + const char *lang; + + HTSMSG_FOREACH(f, tags) { + if (!strcmp(f->hmf_name, tname) && (e = htsmsg_get_map_by_field(f))) { + if (!*ls) *ls = lang_str_create(); + lang = NULL; + if ((attrib = htsmsg_get_map(e, "attrib"))) + lang = htsmsg_get_str(attrib, "lang"); + lang_str_add(*ls, htsmsg_get_str(e, "cdata"), lang, 0); + } + } +} + /** * Parse tags inside of a programme */ @@ -338,11 +360,9 @@ static int _xmltv_parse_programme_tags int sn = 0, sc = 0, en = 0, ec = 0, pn = 0, pc = 0; const char *onscreen = NULL; char *suri = NULL, *uri = NULL; - const char *title = htsmsg_xml_get_cdata_str(tags, "title"); - const char *desc = htsmsg_xml_get_cdata_str(tags, "desc"); - - /* Ignore */ - if (!title) return 0; + lang_str_t *title = NULL; + lang_str_t *desc = NULL; + lang_str_ele_t *ls; /* * Broadcast @@ -369,6 +389,8 @@ static int _xmltv_parse_programme_tags /* Get episode info */ get_episode_info(mod, tags, &uri, &suri, &onscreen, &sn, &sc, &en, &ec, &pn, &pc); + _xmltv_parse_lang_str(&title, tags, "title"); + _xmltv_parse_lang_str(&desc, tags, "desc"); /* * Season @@ -383,12 +405,15 @@ static int _xmltv_parse_programme_tags /* * Episode */ - if (!uri) - uri = epg_hash(title, NULL, desc); + if (!uri && title) { + uri = epg_hash(lang_str_get(title, NULL), NULL, lang_str_get(desc, NULL)); + } if (uri) { ee = epg_episode_find_by_uri(uri, 1, &save); free(uri); } + + /* Update */ if (ee) { stats->episodes.total++; if (save) stats->episodes.created++; @@ -397,10 +422,17 @@ static int _xmltv_parse_programme_tags if (es) save |= epg_episode_set_season(ee, es, mod); - if (title) - save |= epg_episode_set_title(ee, title, mod); - if (desc) - save |= epg_episode_set_description(ee, desc, mod); + if (title) { + RB_FOREACH(ls, title, link) { + save |= epg_episode_set_title(ee, ls->str, ls->lang, mod); + } + } + if (desc) { + RB_FOREACH(ls, desc, link) { + save |= epg_episode_set_description(ee, ls->str, ls->lang, mod); + } + } + if ((egl = _xmltv_parse_categories(tags))) { save |= epg_episode_set_genre(ee, egl, mod); epg_genre_list_destroy(egl); @@ -417,6 +449,10 @@ static int _xmltv_parse_programme_tags /* Stats */ if (save2) stats->broadcasts.modified++; + /* Cleanup */ + if (title) lang_str_destroy(title); + if (desc) lang_str_destroy(desc); + return save | save2 | save3; } diff --git a/src/htsp.c b/src/htsp.c index 5c0828f3..9948bbbf 100644 --- a/src/htsp.c +++ b/src/htsp.c @@ -382,10 +382,10 @@ htsp_build_dvrentry(dvr_entry_t *de, const char *method) htsmsg_add_s32(out, "start", de->de_start); htsmsg_add_s32(out, "stop", de->de_stop); - if( de->de_title != NULL ) - htsmsg_add_str(out, "title", de->de_title); - if( de->de_desc != NULL ) - htsmsg_add_str(out, "description", de->de_desc); + if( de->de_title && (s = lang_str_get(de->de_title, NULL))) + htsmsg_add_str(out, "title", s); + if( de->de_desc && (s = lang_str_get(de->de_desc, NULL))) + htsmsg_add_str(out, "description", s); switch(de->de_sched_state) { case DVR_SCHEDULED: @@ -629,17 +629,11 @@ htsp_method_updateDvrEntry(htsp_connection_t *htsp, htsmsg_t *in) if( (de = dvr_entry_find_by_id(dvrEntryId)) == NULL) return htsp_error("id not found"); - if(htsmsg_get_u32(in, "start", &start)) - start = de->de_start; - - if(htsmsg_get_u32(in, "stop", &stop)) - stop = de->de_stop; - + start = htsmsg_get_u32_or_default(in, "start", 0); + stop = htsmsg_get_u32_or_default(in, "stop", 0); title = htsmsg_get_str(in, "title"); - if (title == NULL) - title = de->de_title; - de = dvr_entry_update(de, title, start, stop); + de = dvr_entry_update(de, title, NULL, start, stop); //create response out = htsmsg_create_map(); @@ -730,7 +724,7 @@ htsp_method_epgQuery(htsp_connection_t *htsp, htsmsg_t *in) } //do the query - epg_query0(&eqr, ch, ct, eg, query); + epg_query0(&eqr, ch, ct, eg, query, NULL); c = eqr.eqr_entries; // create reply @@ -758,6 +752,7 @@ htsp_build_event(epg_broadcast_t *e) epg_broadcast_t *n; dvr_entry_t *de; epg_genre_t *g; + const char *str; out = htsmsg_create_map(); @@ -766,12 +761,12 @@ htsp_build_event(epg_broadcast_t *e) htsmsg_add_u32(out, "start", e->start); htsmsg_add_u32(out, "stop", e->stop); if (e->episode) { - if(e->episode->title != NULL) - htsmsg_add_str(out, "title", e->episode->title); - if(e->episode->description != NULL) - htsmsg_add_str(out, "description", e->episode->description); - else if(e->episode->summary != NULL) - htsmsg_add_str(out, "description", e->episode->summary); + if ((str = epg_episode_get_title(e->episode, NULL))) + htsmsg_add_str(out, "title", str); + if ((str = epg_episode_get_description(e->episode, NULL))) + htsmsg_add_str(out, "description", str); + else if((str = epg_episode_get_summary(e->episode, NULL))) + htsmsg_add_str(out, "description", str); if((g = LIST_FIRST(&e->episode->genre))) htsmsg_add_u32(out, "contentType", g->code); diff --git a/src/htsstr.c b/src/htsstr.c index f745e4ce..9671eea1 100644 --- a/src/htsstr.c +++ b/src/htsstr.c @@ -24,7 +24,7 @@ static void htsstr_argsplit_add(char ***argv, int *argc, char *s); -static int htsstr_format0(const char *str, char *out, char **map); +static int htsstr_format0(const char *str, char *out, const char **map); char * hts_strndup(const char *src, size_t len) @@ -150,9 +150,9 @@ htsstr_argsplit_free(char **argv) { } static int -htsstr_format0(const char *str, char *out, char **map) { +htsstr_format0(const char *str, char *out, const char **map) { const char *s = str; - char *f; + const char *f; int n = 0; while(*s) { @@ -183,7 +183,7 @@ htsstr_format0(const char *str, char *out, char **map) { } char * -htsstr_format(const char *str, char **map) +htsstr_format(const char *str, const char **map) { char *s; diff --git a/src/htsstr.h b/src/htsstr.h index f079c7ec..971ee6be 100644 --- a/src/htsstr.h +++ b/src/htsstr.h @@ -29,6 +29,6 @@ char **htsstr_argsplit(const char *str); void htsstr_argsplit_free(char **argv); -char *htsstr_format(const char *str, char **map); +char *htsstr_format(const char *str, const char **map); #endif /* HTSSTR_H__ */ diff --git a/src/lang_codes.c b/src/lang_codes.c new file mode 100644 index 00000000..cbd4dac9 --- /dev/null +++ b/src/lang_codes.c @@ -0,0 +1,561 @@ +/* + * Multi-language Support - language codes + * Copyright (C) 2012 Adam Sutton + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "lang_codes.h" +#include "config2.h" + +/* ************************************************************************** + * Code list + * *************************************************************************/ + +const lang_code_t lang_codes[] = { + { "und", NULL, NULL , "Undetermined" }, + { "aar", "aa", NULL , "Afar" }, + { "abk", "ab", NULL , "Abkhazian" }, + { "ace", NULL, NULL , "Achinese" }, + { "ach", NULL, NULL , "Acoli" }, + { "ada", NULL, NULL , "Adangme" }, + { "ady", NULL, NULL , "Adyghe; Adygei" }, + { "afa", NULL, NULL , "Afro-Asiatic languages" }, + { "afh", NULL, NULL , "Afrihili" }, + { "afr", "af", NULL , "Afrikaans" }, + { "ain", NULL, NULL , "Ainu" }, + { "aka", "ak", NULL , "Akan" }, + { "akk", NULL, NULL , "Akkadian" }, + { "alb", "sq", "sqi", "Albanian" }, + { "ale", NULL, NULL , "Aleut" }, + { "alg", NULL, NULL , "Algonquian languages" }, + { "alt", NULL, NULL , "Southern Altai" }, + { "amh", "am", NULL , "Amharic" }, + { "anp", NULL, NULL , "Angika" }, + { "apa", NULL, NULL , "Apache languages" }, + { "ara", "ar", NULL , "Arabic" }, + { "arc", NULL, NULL , "Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)" }, + { "arg", "an", NULL , "Aragonese" }, + { "arm", "hy", "hye", "Armenian" }, + { "arn", NULL, NULL , "Mapudungun; Mapuche" }, + { "arp", NULL, NULL , "Arapaho" }, + { "art", NULL, NULL , "Artificial languages" }, + { "arw", NULL, NULL , "Arawak" }, + { "asm", "as", NULL , "Assamese" }, + { "ast", NULL, NULL , "Asturian; Bable; Leonese; Asturleonese" }, + { "ath", NULL, NULL , "Athapascan languages" }, + { "aus", NULL, NULL , "Australian languages" }, + { "ava", "av", NULL , "Avaric" }, + { "ave", "ae", NULL , "Avestan" }, + { "awa", NULL, NULL , "Awadhi" }, + { "aym", "ay", NULL , "Aymara" }, + { "aze", "az", NULL , "Azerbaijani" }, + { "bad", NULL, NULL , "Banda languages" }, + { "bai", NULL, NULL , "Bamileke languages" }, + { "bak", "ba", NULL , "Bashkir" }, + { "bal", NULL, NULL , "Baluchi" }, + { "bam", "bm", NULL , "Bambara" }, + { "ban", NULL, NULL , "Balinese" }, + { "baq", "eu", "eus", "Basque" }, + { "bas", NULL, NULL , "Basa" }, + { "bat", NULL, NULL , "Baltic languages" }, + { "bej", NULL, NULL , "Beja; Bedawiyet" }, + { "bel", "be", NULL , "Belarusian" }, + { "bem", NULL, NULL , "Bemba" }, + { "ben", "bn", NULL , "Bengali" }, + { "ber", NULL, NULL , "Berber languages" }, + { "bho", NULL, NULL , "Bhojpuri" }, + { "bih", "bh", NULL , "Bihari languages" }, + { "bik", NULL, NULL , "Bikol" }, + { "bin", NULL, NULL , "Bini; Edo" }, + { "bis", "bi", NULL , "Bislama" }, + { "bla", NULL, NULL , "Siksika" }, + { "bnt", NULL, NULL , "Bantu languages" }, + { "bos", "bs", NULL , "Bosnian" }, + { "bra", NULL, NULL , "Braj" }, + { "bre", "br", NULL , "Breton" }, + { "btk", NULL, NULL , "Batak languages" }, + { "bua", NULL, NULL , "Buriat" }, + { "bug", NULL, NULL , "Buginese" }, + { "bul", "bg", NULL , "Bulgarian" }, + { "bur", "my", "mya", "Burmese" }, + { "byn", NULL, NULL , "Blin; Bilin" }, + { "cad", NULL, NULL , "Caddo" }, + { "cai", NULL, NULL , "Central American Indian languages" }, + { "car", NULL, NULL , "Galibi Carib" }, + { "cat", "ca", NULL , "Catalan; Valencian" }, + { "cau", NULL, NULL , "Caucasian languages" }, + { "ceb", NULL, NULL , "Cebuano" }, + { "cel", NULL, NULL , "Celtic languages" }, + { "cha", "ch", NULL , "Chamorro" }, + { "chb", NULL, NULL , "Chibcha" }, + { "che", "ce", NULL , "Chechen" }, + { "chg", NULL, NULL , "Chagatai" }, + { "chi", "zh", "zho", "Chinese" }, + { "chk", NULL, NULL , "Chuukese" }, + { "chm", NULL, NULL , "Mari" }, + { "chn", NULL, NULL , "Chinook jargon" }, + { "cho", NULL, NULL , "Choctaw" }, + { "chp", NULL, NULL , "Chipewyan; Dene Suline" }, + { "chr", NULL, NULL , "Cherokee" }, + { "chu", "cu", NULL , "Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic" }, + { "chv", "cv", NULL , "Chuvash" }, + { "chy", NULL, NULL , "Cheyenne" }, + { "cmc", NULL, NULL , "Chamic languages" }, + { "cop", NULL, NULL , "Coptic" }, + { "cor", "kw", NULL , "Cornish" }, + { "cos", "co", NULL , "Corsican" }, + { "cre", "cr", NULL , "Cree" }, + { "crh", NULL, NULL , "Crimean Tatar; Crimean Turkish" }, + { "crp", NULL, NULL , "Creoles and pidgins" }, + { "csb", NULL, NULL , "Kashubian" }, + { "cus", NULL, NULL , "Cushitic languages" }, + { "cze", "cs", "ces", "Czech" }, + { "dak", NULL, NULL , "Dakota" }, + { "dan", "da", NULL , "Danish" }, + { "dar", NULL, NULL , "Dargwa" }, + { "day", NULL, NULL , "Land Dayak languages" }, + { "del", NULL, NULL , "Delaware" }, + { "den", NULL, NULL , "Slave (Athapascan)" }, + { "dgr", NULL, NULL , "Dogrib" }, + { "din", NULL, NULL , "Dinka" }, + { "div", "dv", NULL , "Divehi; Dhivehi; Maldivian" }, + { "doi", NULL, NULL , "Dogri" }, + { "dra", NULL, NULL , "Dravidian languages" }, + { "dsb", NULL, NULL , "Lower Sorbian" }, + { "dua", NULL, NULL , "Duala" }, + { "dut", "nl", "nld", "Dutch; Flemish" }, + { "dyu", NULL, NULL , "Dyula" }, + { "dzo", "dz", NULL , "Dzongkha" }, + { "efi", NULL, NULL , "Efik" }, + { "egy", NULL, NULL , "Egyptian (Ancient)" }, + { "eka", NULL, NULL , "Ekajuk" }, + { "elx", NULL, NULL , "Elamite" }, + { "eng", "en", NULL , "English" }, + { "epo", "eo", NULL , "Esperanto" }, + { "est", "et", NULL , "Estonian" }, + { "ewe", "ee", NULL , "Ewe" }, + { "ewo", NULL, NULL , "Ewondo" }, + { "fan", NULL, NULL , "Fang" }, + { "fao", "fo", NULL , "Faroese" }, + { "fat", NULL, NULL , "Fanti" }, + { "fij", "fj", NULL , "Fijian" }, + { "fil", NULL, NULL , "Filipino; Pilipino" }, + { "fin", "fi", NULL , "Finnish" }, + { "fiu", NULL, NULL , "Finno-Ugrian languages" }, + { "fon", NULL, NULL , "Fon" }, + { "fre", "fr", "fra", "French" }, + { "frr", NULL, NULL , "Northern Frisian" }, + { "frs", NULL, NULL , "Eastern Frisian" }, + { "fry", "fy", NULL , "Western Frisian" }, + { "ful", "ff", NULL , "Fulah" }, + { "fur", NULL, NULL , "Friulian" }, + { "gaa", NULL, NULL , "Ga" }, + { "gay", NULL, NULL , "Gayo" }, + { "gba", NULL, NULL , "Gbaya" }, + { "gem", NULL, NULL , "Germanic languages" }, + { "geo", "ka", "kat", "Georgian" }, + { "ger", "de", "deu", "German" }, + { "gez", NULL, NULL , "Geez" }, + { "gil", NULL, NULL , "Gilbertese" }, + { "gla", "gd", NULL , "Gaelic; Scottish Gaelic" }, + { "gle", "ga", NULL , "Irish" }, + { "glg", "gl", NULL , "Galician" }, + { "glv", "gv", NULL , "Manx" }, + { "gon", NULL, NULL , "Gondi" }, + { "gor", NULL, NULL , "Gorontalo" }, + { "got", NULL, NULL , "Gothic" }, + { "grb", NULL, NULL , "Grebo" }, + { "grn", "gn", NULL , "Guarani" }, + { "gsw", NULL, NULL , "Swiss German; Alemannic; Alsatian" }, + { "guj", "gu", NULL , "Gujarati" }, + { "gwi", NULL, NULL , "Gwich'in" }, + { "hai", NULL, NULL , "Haida" }, + { "hat", "ht", NULL , "Haitian; Haitian Creole" }, + { "hau", "ha", NULL , "Hausa" }, + { "haw", NULL, NULL , "Hawaiian" }, + { "heb", "he", NULL , "Hebrew" }, + { "her", "hz", NULL , "Herero" }, + { "hil", NULL, NULL , "Hiligaynon" }, + { "him", NULL, NULL , "Himachali languages; Western Pahari languages" }, + { "hin", "hi", NULL , "Hindi" }, + { "hit", NULL, NULL , "Hittite" }, + { "hmn", NULL, NULL , "Hmong; Mong" }, + { "hmo", "ho", NULL , "Hiri Motu" }, + { "hrv", "hr", NULL , "Croatian" }, + { "hsb", NULL, NULL , "Upper Sorbian" }, + { "hun", "hu", NULL , "Hungarian" }, + { "hup", NULL, NULL , "Hupa" }, + { "iba", NULL, NULL , "Iban" }, + { "ibo", "ig", NULL , "Igbo" }, + { "ice", "is", "isl", "Icelandic" }, + { "ido", "io", NULL , "Ido" }, + { "iii", "ii", NULL , "Sichuan Yi; Nuosu" }, + { "ijo", NULL, NULL , "Ijo languages" }, + { "iku", "iu", NULL , "Inuktitut" }, + { "ile", "ie", NULL , "Interlingue; Occidental" }, + { "ilo", NULL, NULL , "Iloko" }, + { "ina", "ia", NULL , "Interlingua (International Auxiliary Language Association)" }, + { "inc", NULL, NULL , "Indic languages" }, + { "ind", "id", NULL , "Indonesian" }, + { "ine", NULL, NULL , "Indo-European languages" }, + { "inh", NULL, NULL , "Ingush" }, + { "ipk", "ik", NULL , "Inupiaq" }, + { "ira", NULL, NULL , "Iranian languages" }, + { "iro", NULL, NULL , "Iroquoian languages" }, + { "ita", "it", NULL , "Italian" }, + { "jav", "jv", NULL , "Javanese" }, + { "jbo", NULL, NULL , "Lojban" }, + { "jpn", "ja", NULL , "Japanese" }, + { "jpr", NULL, NULL , "Judeo-Persian" }, + { "jrb", NULL, NULL , "Judeo-Arabic" }, + { "kaa", NULL, NULL , "Kara-Kalpak" }, + { "kab", NULL, NULL , "Kabyle" }, + { "kac", NULL, NULL , "Kachin; Jingpho" }, + { "kal", "kl", NULL , "Kalaallisut; Greenlandic" }, + { "kam", NULL, NULL , "Kamba" }, + { "kan", "kn", NULL , "Kannada" }, + { "kar", NULL, NULL , "Karen languages" }, + { "kas", "ks", NULL , "Kashmiri" }, + { "kau", "kr", NULL , "Kanuri" }, + { "kaw", NULL, NULL , "Kawi" }, + { "kaz", "kk", NULL , "Kazakh" }, + { "kbd", NULL, NULL , "Kabardian" }, + { "kha", NULL, NULL , "Khasi" }, + { "khi", NULL, NULL , "Khoisan languages" }, + { "khm", "km", NULL , "Central Khmer" }, + { "kho", NULL, NULL , "Khotanese; Sakan" }, + { "kik", "ki", NULL , "Kikuyu; Gikuyu" }, + { "kin", "rw", NULL , "Kinyarwanda" }, + { "kir", "ky", NULL , "Kirghiz; Kyrgyz" }, + { "kmb", NULL, NULL , "Kimbundu" }, + { "kok", NULL, NULL , "Konkani" }, + { "kom", "kv", NULL , "Komi" }, + { "kon", "kg", NULL , "Kongo" }, + { "kor", "ko", NULL , "Korean" }, + { "kos", NULL, NULL , "Kosraean" }, + { "kpe", NULL, NULL , "Kpelle" }, + { "krc", NULL, NULL , "Karachay-Balkar" }, + { "krl", NULL, NULL , "Karelian" }, + { "kro", NULL, NULL , "Kru languages" }, + { "kru", NULL, NULL , "Kurukh" }, + { "kua", "kj", NULL , "Kuanyama; Kwanyama" }, + { "kum", NULL, NULL , "Kumyk" }, + { "kur", "ku", NULL , "Kurdish" }, + { "kut", NULL, NULL , "Kutenai" }, + { "lad", NULL, NULL , "Ladino" }, + { "lah", NULL, NULL , "Lahnda" }, + { "lam", NULL, NULL , "Lamba" }, + { "lao", "lo", NULL , "Lao" }, + { "lat", "la", NULL , "Latin" }, + { "lav", "lv", NULL , "Latvian" }, + { "lez", NULL, NULL , "Lezghian" }, + { "lim", "li", NULL , "Limburgan; Limburger; Limburgish" }, + { "lin", "ln", NULL , "Lingala" }, + { "lit", "lt", NULL , "Lithuanian" }, + { "lol", NULL, NULL , "Mongo" }, + { "loz", NULL, NULL , "Lozi" }, + { "ltz", "lb", NULL , "Luxembourgish; Letzeburgesch" }, + { "lua", NULL, NULL , "Luba-Lulua" }, + { "lub", "lu", NULL , "Luba-Katanga" }, + { "lug", "lg", NULL , "Ganda" }, + { "lui", NULL, NULL , "Luiseno" }, + { "lun", NULL, NULL , "Lunda" }, + { "luo", NULL, NULL , "Luo (Kenya and Tanzania)" }, + { "lus", NULL, NULL , "Lushai" }, + { "mac", "mk", "mkd", "Macedonian" }, + { "mad", NULL, NULL , "Madurese" }, + { "mag", NULL, NULL , "Magahi" }, + { "mah", "mh", NULL , "Marshallese" }, + { "mai", NULL, NULL , "Maithili" }, + { "mak", NULL, NULL , "Makasar" }, + { "mal", "ml", NULL , "Malayalam" }, + { "man", NULL, NULL , "Mandingo" }, + { "mao", "mi", "mri", "Maori" }, + { "map", NULL, NULL , "Austronesian languages" }, + { "mar", "mr", NULL , "Marathi" }, + { "mas", NULL, NULL , "Masai" }, + { "may", "ms", "msa", "Malay" }, + { "mdf", NULL, NULL , "Moksha" }, + { "mdr", NULL, NULL , "Mandar" }, + { "men", NULL, NULL , "Mende" }, + { "mic", NULL, NULL , "Mi'kmaq; Micmac" }, + { "min", NULL, NULL , "Minangkabau" }, + { "mis", NULL, NULL , "Uncoded languages" }, + { "mkh", NULL, NULL , "Mon-Khmer languages" }, + { "mlg", "mg", NULL , "Malagasy" }, + { "mlt", "mt", NULL , "Maltese" }, + { "mnc", NULL, NULL , "Manchu" }, + { "mni", NULL, NULL , "Manipuri" }, + { "mno", NULL, NULL , "Manobo languages" }, + { "moh", NULL, NULL , "Mohawk" }, + { "mon", "mn", NULL , "Mongolian" }, + { "mos", NULL, NULL , "Mossi" }, + { "mul", NULL, NULL , "Multiple languages" }, + { "mun", NULL, NULL , "Munda languages" }, + { "mus", NULL, NULL , "Creek" }, + { "mwl", NULL, NULL , "Mirandese" }, + { "mwr", NULL, NULL , "Marwari" }, + { "myn", NULL, NULL , "Mayan languages" }, + { "myv", NULL, NULL , "Erzya" }, + { "nah", NULL, NULL , "Nahuatl languages" }, + { "nai", NULL, NULL , "North American Indian languages" }, + { "nap", NULL, NULL , "Neapolitan" }, + { "nau", "na", NULL , "Nauru" }, + { "nav", "nv", NULL , "Navajo; Navaho" }, + { "ndo", "ng", NULL , "Ndonga" }, + { "nep", "ne", NULL , "Nepali" }, + { "new", NULL, NULL , "Nepal Bhasa; Newari" }, + { "nia", NULL, NULL , "Nias" }, + { "nic", NULL, NULL , "Niger-Kordofanian languages" }, + { "niu", NULL, NULL , "Niuean" }, + { "nog", NULL, NULL , "Nogai" }, + { "nor", "no", NULL , "Norwegian" }, + { "nqo", NULL, NULL , "N'Ko" }, + { "nso", NULL, NULL , "Pedi; Sepedi; Northern Sotho" }, + { "nub", NULL, NULL , "Nubian languages" }, + { "nwc", NULL, NULL , "Classical Newari; Old Newari; Classical Nepal Bhasa" }, + { "nya", "ny", NULL , "Chichewa; Chewa; Nyanja" }, + { "nym", NULL, NULL , "Nyamwezi" }, + { "nyn", NULL, NULL , "Nyankole" }, + { "nyo", NULL, NULL , "Nyoro" }, + { "nzi", NULL, NULL , "Nzima" }, + { "oci", "oc", NULL , "Occitan (post 1500)" }, + { "oji", "oj", NULL , "Ojibwa" }, + { "ori", "or", NULL , "Oriya" }, + { "orm", "om", NULL , "Oromo" }, + { "osa", NULL, NULL , "Osage" }, + { "oss", "os", NULL , "Ossetian; Ossetic" }, + { "oto", NULL, NULL , "Otomian languages" }, + { "paa", NULL, NULL , "Papuan languages" }, + { "pag", NULL, NULL , "Pangasinan" }, + { "pal", NULL, NULL , "Pahlavi" }, + { "pam", NULL, NULL , "Pampanga; Kapampangan" }, + { "pan", "pa", NULL , "Panjabi; Punjabi" }, + { "pap", NULL, NULL , "Papiamento" }, + { "pau", NULL, NULL , "Palauan" }, + { "per", "fa", "fas", "Persian" }, + { "phi", NULL, NULL , "Philippine languages" }, + { "phn", NULL, NULL , "Phoenician" }, + { "pli", "pi", NULL , "Pali" }, + { "pol", "pl", NULL , "Polish" }, + { "pon", NULL, NULL , "Pohnpeian" }, + { "por", "pt", NULL , "Portuguese" }, + { "pra", NULL, NULL , "Prakrit languages" }, + { "pus", "ps", NULL , "Pushto; Pashto" }, + { "que", "qu", NULL , "Quechua" }, + { "raj", NULL, NULL , "Rajasthani" }, + { "rap", NULL, NULL , "Rapanui" }, + { "rar", NULL, NULL , "Rarotongan; Cook Islands Maori" }, + { "roa", NULL, NULL , "Romance languages" }, + { "roh", "rm", NULL , "Romansh" }, + { "rom", NULL, NULL , "Romany" }, + { "rum", "ro", "ron", "Romanian; Moldavian; Moldovan" }, + { "run", "rn", NULL , "Rundi" }, + { "rup", NULL, NULL , "Aromanian; Arumanian; Macedo-Romanian" }, + { "rus", "ru", NULL , "Russian" }, + { "sad", NULL, NULL , "Sandawe" }, + { "sag", "sg", NULL , "Sango" }, + { "sah", NULL, NULL , "Yakut" }, + { "sai", NULL, NULL , "South American Indian languages" }, + { "sal", NULL, NULL , "Salishan languages" }, + { "sam", NULL, NULL , "Samaritan Aramaic" }, + { "san", "sa", NULL , "Sanskrit" }, + { "sas", NULL, NULL , "Sasak" }, + { "sat", NULL, NULL , "Santali" }, + { "scn", NULL, NULL , "Sicilian" }, + { "sco", NULL, NULL , "Scots" }, + { "sel", NULL, NULL , "Selkup" }, + { "sem", NULL, NULL , "Semitic languages" }, + { "sgn", NULL, NULL , "Sign Languages" }, + { "shn", NULL, NULL , "Shan" }, + { "sid", NULL, NULL , "Sidamo" }, + { "sin", "si", NULL , "Sinhala; Sinhalese" }, + { "sio", NULL, NULL , "Siouan languages" }, + { "sit", NULL, NULL , "Sino-Tibetan languages" }, + { "sla", NULL, NULL , "Slavic languages" }, + { "slo", "sk", "slk", "Slovak" }, + { "slv", "sl", NULL , "Slovenian" }, + { "sma", NULL, NULL , "Southern Sami" }, + { "sme", "se", NULL , "Northern Sami" }, + { "smi", NULL, NULL , "Sami languages" }, + { "smj", NULL, NULL , "Lule Sami" }, + { "smn", NULL, NULL , "Inari Sami" }, + { "smo", "sm", NULL , "Samoan" }, + { "sms", NULL, NULL , "Skolt Sami" }, + { "sna", "sn", NULL , "Shona" }, + { "snd", "sd", NULL , "Sindhi" }, + { "snk", NULL, NULL , "Soninke" }, + { "sog", NULL, NULL , "Sogdian" }, + { "som", "so", NULL , "Somali" }, + { "son", NULL, NULL , "Songhai languages" }, + { "spa", "es", NULL , "Spanish; Castilian" }, + { "srd", "sc", NULL , "Sardinian" }, + { "srn", NULL, NULL , "Sranan Tongo" }, + { "srp", "sr", NULL , "Serbian" }, + { "srr", NULL, NULL , "Serer" }, + { "ssa", NULL, NULL , "Nilo-Saharan languages" }, + { "ssw", "ss", NULL , "Swati" }, + { "suk", NULL, NULL , "Sukuma" }, + { "sun", "su", NULL , "Sundanese" }, + { "sus", NULL, NULL , "Susu" }, + { "sux", NULL, NULL , "Sumerian" }, + { "swa", "sw", NULL , "Swahili" }, + { "swe", "sv", NULL , "Swedish" }, + { "syc", NULL, NULL , "Classical Syriac" }, + { "syr", NULL, NULL , "Syriac" }, + { "tah", "ty", NULL , "Tahitian" }, + { "tai", NULL, NULL , "Tai languages" }, + { "tam", "ta", NULL , "Tamil" }, + { "tat", "tt", NULL , "Tatar" }, + { "tel", "te", NULL , "Telugu" }, + { "tem", NULL, NULL , "Timne" }, + { "ter", NULL, NULL , "Tereno" }, + { "tet", NULL, NULL , "Tetum" }, + { "tgk", "tg", NULL , "Tajik" }, + { "tgl", "tl", NULL , "Tagalog" }, + { "tha", "th", NULL , "Thai" }, + { "tib", "bo", "bod", "Tibetan" }, + { "tig", NULL, NULL , "Tigre" }, + { "tir", "ti", NULL , "Tigrinya" }, + { "tiv", NULL, NULL , "Tiv" }, + { "tkl", NULL, NULL , "Tokelau" }, + { "tlh", NULL, NULL , "Klingon; tlhIngan-Hol" }, + { "tli", NULL, NULL , "Tlingit" }, + { "tmh", NULL, NULL , "Tamashek" }, + { "tog", NULL, NULL , "Tonga (Nyasa)" }, + { "ton", "to", NULL , "Tonga (Tonga Islands)" }, + { "tpi", NULL, NULL , "Tok Pisin" }, + { "tsi", NULL, NULL , "Tsimshian" }, + { "tsn", "tn", NULL , "Tswana" }, + { "tso", "ts", NULL , "Tsonga" }, + { "tuk", "tk", NULL , "Turkmen" }, + { "tum", NULL, NULL , "Tumbuka" }, + { "tup", NULL, NULL , "Tupi languages" }, + { "tur", "tr", NULL , "Turkish" }, + { "tut", NULL, NULL , "Altaic languages" }, + { "tvl", NULL, NULL , "Tuvalu" }, + { "twi", "tw", NULL , "Twi" }, + { "tyv", NULL, NULL , "Tuvinian" }, + { "udm", NULL, NULL , "Udmurt" }, + { "uga", NULL, NULL , "Ugaritic" }, + { "uig", "ug", NULL , "Uighur; Uyghur" }, + { "ukr", "uk", NULL , "Ukrainian" }, + { "umb", NULL, NULL , "Umbundu" }, + { "urd", "ur", NULL , "Urdu" }, + { "uzb", "uz", NULL , "Uzbek" }, + { "vai", NULL, NULL , "Vai" }, + { "ven", "ve", NULL , "Venda" }, + { "vie", "vi", NULL , "Vietnamese" }, + { "vol", "vo", NULL , "Volapük" }, + { "vot", NULL, NULL , "Votic" }, + { "wak", NULL, NULL , "Wakashan languages" }, + { "wal", NULL, NULL , "Wolaitta; Wolaytta" }, + { "war", NULL, NULL , "Waray" }, + { "was", NULL, NULL , "Washo" }, + { "wel", "cy", "cym", "Welsh" }, + { "wen", NULL, NULL , "Sorbian languages" }, + { "wln", "wa", NULL , "Walloon" }, + { "wol", "wo", NULL , "Wolof" }, + { "xal", NULL, NULL , "Kalmyk; Oirat" }, + { "xho", "xh", NULL , "Xhosa" }, + { "yao", NULL, NULL , "Yao" }, + { "yap", NULL, NULL , "Yapese" }, + { "yid", "yi", NULL , "Yiddish" }, + { "yor", "yo", NULL , "Yoruba" }, + { "ypk", NULL, NULL , "Yupik languages" }, + { "zap", NULL, NULL , "Zapotec" }, + { "zbl", NULL, NULL , "Blissymbols; Blissymbolics; Bliss" }, + { "zen", NULL, NULL , "Zenaga" }, + { "zha", "za", NULL , "Zhuang; Chuang" }, + { "znd", NULL, NULL , "Zande languages" }, + { "zul", "zu", NULL , "Zulu" }, + { "zun", NULL, NULL , "Zuni" }, + { "zza", NULL, NULL , "Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki" }, + { NULL, NULL, NULL, NULL } +}; + +/* ************************************************************************** + * Functions + * *************************************************************************/ + +const char *lang_code_get ( const char *code ) +{ + int i; + char tmp[4]; + + if (code && *code) { + + /* Extract the code (lowercase) */ + i = 0; + while (i < 3 && *code) { + if (*code == ';' || *code == ',' || *code == '-') break; + if (*code != ' ') + tmp[i++] = *code | 0x20; // |0x20 = lower case + code++; + } + tmp[i] = '\0'; + + /* Search */ + if (i) { + const lang_code_t *c = lang_codes; + while (c->code2b) { + if ( !strcmp(tmp, c->code2b) ) return c->code2b; + if ( c->code1 && !strcmp(tmp, c->code1) ) return c->code2b; + if ( c->code2t && !strcmp(tmp, c->code2t) ) return c->code2b; + c++; + } + } + } + return lang_codes[0].code2b; +} + +const char **lang_code_split ( const char *codes ) +{ + int n; + const char *c, *p, **ret; + + /* Defaults */ + if (!codes) codes = config_get_language(); + + /* No config */ + if (!codes) return NULL; + + /* Count entries */ + n = 0; + c = codes; + while (*c) { + if (*c == ',') n++; + c++; + } + ret = calloc(2+n, sizeof(char*)); + + /* Create list */ + n = 0; + p = c = codes; + while (*c) { + if (*c == ',') { + ret[n++] = lang_code_get(p); + p = c + 1; + } + c++; + } + if (*p) ret[n++] = lang_code_get(p); + ret[n] = NULL; + + return ret; +} diff --git a/src/lang_codes.h b/src/lang_codes.h new file mode 100644 index 00000000..695ba6a4 --- /dev/null +++ b/src/lang_codes.h @@ -0,0 +1,38 @@ +/* + * Multi-language Support - language codes + * Copyright (C) 2012 Adam Sutton + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __TVH_LANG_CODES_H__ +#define __TVH_LANG_CODES_H__ + +typedef struct lang_code +{ + const char *code2b; ///< ISO 639-2 B + const char *code1; ///< ISO 639-1 + const char *code2t; ///< ISO 639-2 T + const char *desc; ///< Description +} lang_code_t; + +extern const lang_code_t lang_codes[]; + +/* Convert code to preferred internal code */ +const char *lang_code_get ( const char *code ); + +/* Split list of codes as per HTTP Language-Accept spec */ +const char **lang_code_split ( const char *codes ); + +#endif /* __TVH_LANG_CODES_H__ */ diff --git a/src/lang_str.c b/src/lang_str.c new file mode 100644 index 00000000..04508c7a --- /dev/null +++ b/src/lang_str.c @@ -0,0 +1,191 @@ +/* + * Multi-language String support + * Copyright (C) 2012 Adam Sutton + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "redblack.h" +#include "lang_codes.h" +#include "lang_str.h" + +/* ************************************************************************ + * Support + * ***********************************************************************/ + +/* Compare language codes */ +static int _lang_cmp ( void *a, void *b ) +{ + return strcmp(((lang_str_ele_t*)a)->lang, ((lang_str_ele_t*)b)->lang); +} + +/* ************************************************************************ + * Language String + * ***********************************************************************/ + +/* Create new instance */ +lang_str_t *lang_str_create ( void ) +{ + return calloc(1, sizeof(lang_str_t)); +} + +/* Destroy (free memory) */ +void lang_str_destroy ( lang_str_t *ls ) +{ + lang_str_ele_t *e; + while ((e = RB_FIRST(ls))) { + if (e->str) free(e->str); + RB_REMOVE(ls, e, link); + free(e); + } + free(ls); +} + +/* Copy the lang_str instance */ +lang_str_t *lang_str_copy ( const lang_str_t *ls ) +{ + lang_str_t *ret = lang_str_create(); + lang_str_ele_t *e; + RB_FOREACH(e, ls, link) + lang_str_add(ret, e->str, e->lang, 0); + return ret; +} + +/* Get language element */ +lang_str_ele_t *lang_str_get2 + ( lang_str_t *ls, const char *lang ) +{ + int i; + const char **langs; + lang_str_ele_t skel, *e = NULL; + + if (!ls) return NULL; + + /* Check config/requested langs */ + if ((langs = lang_code_split(lang))) { + i = 0; + while (langs[i]) { + skel.lang = langs[i]; + if ((e = RB_FIND(ls, &skel, link, _lang_cmp))) + break; + i++; + } + free(langs); + } + + /* Use first available */ + if (!e) e = RB_FIRST(ls); + + /* Return */ + return e; +} + +/* Get string */ +const char *lang_str_get + ( lang_str_t *ls, const char *lang ) +{ + lang_str_ele_t *e = lang_str_get2(ls, lang); + return e ? e->str : NULL; +} + +/* Internal insertion routine */ +static int _lang_str_add + ( lang_str_t *ls, const char *str, const char *lang, int update, int append ) +{ + int save = 0; + static lang_str_ele_t *skel = NULL; + lang_str_ele_t *e; + + if (!str) return 0; + + /* Get proper code */ + if (!(lang = lang_code_get(lang))) return 0; + + /* Create skel */ + if (!skel) skel = calloc(1, sizeof(lang_str_ele_t)); + skel->lang = lang; + + /* Create */ + e = RB_INSERT_SORTED(ls, skel, link, _lang_cmp); + if (!e) { + skel->str = strdup(str); + skel = NULL; + save = 1; + + /* Append */ + } else if (append) { + e->str = realloc(e->str, strlen(e->str) + strlen(str) + 1); + strcat(e->str, str); + save = 1; + + /* Update */ + } else if (update && strcmp(str, e->str)) { + free(e->str); + e->str = strdup(str); + save = 1; + } + + return save; +} + +/* Add new string (or replace existing one) */ +int lang_str_add + ( lang_str_t *ls, const char *str, const char *lang, int update ) +{ + return _lang_str_add(ls, str, lang, update, 0); +} + +/* Append to existing string (or add new one) */ +int lang_str_append + ( lang_str_t *ls, const char *str, const char *lang ) +{ + return _lang_str_add(ls, str, lang, 0, 1); +} + +/* Serialize */ +void lang_str_serialize ( lang_str_t *ls, htsmsg_t *m, const char *f ) +{ + lang_str_ele_t *e; + if (!ls) return; + htsmsg_t *a = htsmsg_create_map(); + RB_FOREACH(e, ls, link) { + htsmsg_add_str(a, e->lang, e->str); + } + htsmsg_add_msg(m, f, a); +} + +/* De-serialize */ +lang_str_t *lang_str_deserialize ( htsmsg_t *m, const char *n ) +{ + lang_str_t *ret = NULL; + htsmsg_t *a; + htsmsg_field_t *f; + const char *str; + + if ((a = htsmsg_get_map(m, n))) { + ret = lang_str_create(); + HTSMSG_FOREACH(f, a) { + if ((str = htsmsg_field_get_string(f))) { + lang_str_add(ret, str, f->hmf_name, 0); + } + } + } else if ((str = htsmsg_get_str(m, n))) { + ret = lang_str_create(); + lang_str_add(ret, str, NULL, 0); + } + return ret; +} diff --git a/src/lang_str.h b/src/lang_str.h new file mode 100644 index 00000000..7c178076 --- /dev/null +++ b/src/lang_str.h @@ -0,0 +1,55 @@ +/* + * Multi-language String support + * Copyright (C) 2012 Adam Sutton + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __TVH_LANG_STR_H__ +#define __TVH_LANG_STR_H__ + +#include "redblack.h" +#include "htsmsg.h" + +typedef struct lang_str_ele +{ + RB_ENTRY(lang_str_ele) link; + const char *lang; + char *str; +} lang_str_ele_t; + +typedef RB_HEAD(lang_str, lang_str_ele) lang_str_t; + +/* Create/Destroy */ +void lang_str_destroy ( lang_str_t *ls ); +lang_str_t *lang_str_create ( void ); +lang_str_t *lang_str_copy ( const lang_str_t *ls ); + +/* Get elements */ +const char *lang_str_get ( lang_str_t *ls, const char *lang ); +lang_str_ele_t *lang_str_get2 ( lang_str_t *ls, const char *lang ); + +/* Add/Update elements */ +int lang_str_add + ( lang_str_t *ls, const char *str, const char *lang, int update ); +int lang_str_append + ( lang_str_t *ls, const char *str, const char *lang ); + +/* Serialize/Deserialize */ +void lang_str_serialize + ( lang_str_t *ls, htsmsg_t *msg, const char *f ); +lang_str_t *lang_str_deserialize + ( htsmsg_t *m, const char *f ); + +#endif /* __TVH_LANG_STR_H__ */ diff --git a/src/webui/extjs.c b/src/webui/extjs.c index e57035b9..3c5426c7 100644 --- a/src/webui/extjs.c +++ b/src/webui/extjs.c @@ -718,6 +718,7 @@ extjs_epg(http_connection_t *hc, const char *remain, void *opaque) const char *channel = http_arg_get(&hc->hc_req_args, "channel"); const char *tag = http_arg_get(&hc->hc_req_args, "tag"); const char *title = http_arg_get(&hc->hc_req_args, "title"); + const char *lang = http_arg_get(&hc->hc_args, "Accept-Language"); if(channel && !channel[0]) channel = NULL; if(tag && !tag[0]) tag = NULL; @@ -740,7 +741,7 @@ extjs_epg(http_connection_t *hc, const char *remain, void *opaque) pthread_mutex_lock(&global_lock); - epg_query(&eqr, channel, tag, eg, title); + epg_query(&eqr, channel, tag, eg, title, lang); epg_query_sort(&eqr); @@ -763,17 +764,18 @@ extjs_epg(http_connection_t *hc, const char *remain, void *opaque) if(ch->ch_icon != NULL) htsmsg_add_str(m, "chicon", ch->ch_icon); - if(ee->title != NULL) - htsmsg_add_str(m, "title", ee->title); - if(ee->subtitle) - htsmsg_add_str(m, "subtitle", ee->subtitle); + if((s = epg_episode_get_title(ee, lang))) + htsmsg_add_str(m, "title", s); + if((s = epg_episode_get_subtitle(ee, lang))) + htsmsg_add_str(m, "subtitle", s); - if(ee->description != NULL) - htsmsg_add_str(m, "description", ee->description); - else if(ee->summary != NULL) - htsmsg_add_str(m, "description", ee->summary); + if((s = epg_episode_get_description(ee, lang))) + htsmsg_add_str(m, "description", s); + else if((s = epg_episode_get_summary(ee, lang))) + htsmsg_add_str(m, "description", s); - if (epg_episode_number_format(ee, buf, 100, NULL, "Season %d", ".", "Episode %d", "/%d")) + if (epg_episode_number_format(ee, buf, 100, NULL, "Season %d", ".", + "Episode %d", "/%d")) htsmsg_add_str(m, "episode", buf); htsmsg_add_u32(m, "id", e->id); @@ -813,11 +815,12 @@ extjs_epgrelated(http_connection_t *hc, const char *remain, void *opaque) epg_episode_t *ee, *ee2; channel_t *ch; uint32_t count = 0; - const char *id, *type; + const char *s; char buf[100]; - id = http_arg_get(&hc->hc_req_args, "id"); - type = http_arg_get(&hc->hc_req_args, "type"); + const char *lang = http_arg_get(&hc->hc_args, "Accept-Language"); + const char *id = http_arg_get(&hc->hc_req_args, "id"); + const char *type = http_arg_get(&hc->hc_req_args, "type"); out = htsmsg_create_map(); array = htsmsg_create_list(); @@ -852,9 +855,12 @@ extjs_epgrelated(http_connection_t *hc, const char *remain, void *opaque) count++; m = htsmsg_create_map(); htsmsg_add_str(m, "uri", ee2->uri); - htsmsg_add_str(m, "title", ee2->title); - if (ee2->subtitle) htsmsg_add_str(m, "subtitle", ee2->subtitle); - if (epg_episode_number_format(ee2, buf, 100, NULL, "Season %d", ".", "Episode %d", "/%d")) + if ((s = epg_episode_get_title(ee2, lang))) + htsmsg_add_str(m, "title", s); + if ((s = epg_episode_get_subtitle(ee2, lang))) + htsmsg_add_str(m, "subtitle", s); + if (epg_episode_number_format(ee2, buf, 100, NULL, "Season %d", + ".", "Episode %d", "/%d")) htsmsg_add_str(m, "episode", buf); htsmsg_add_msg(array, NULL, m); } @@ -865,9 +871,12 @@ extjs_epgrelated(http_connection_t *hc, const char *remain, void *opaque) count++; m = htsmsg_create_map(); htsmsg_add_str(m, "uri", ee2->uri); - htsmsg_add_str(m, "title", ee2->title); - if (ee2->subtitle) htsmsg_add_str(m, "subtitle", ee2->subtitle); - if (epg_episode_number_format(ee2, buf, 100, NULL, "Season %d", ".", "Episode %d", "/%d")) + if ((s = epg_episode_get_title(ee2, lang))) + htsmsg_add_str(m, "title", s); + if ((s = epg_episode_get_subtitle(ee2, lang))) + htsmsg_add_str(m, "subtitle", s); + if (epg_episode_number_format(ee2, buf, 100, NULL, "Season %d", + ".", "Episode %d", "/%d")) htsmsg_add_str(m, "episode", buf); htsmsg_add_msg(array, NULL, m); } @@ -1235,10 +1244,10 @@ extjs_dvrlist(http_connection_t *hc, const char *remain, void *opaque) htsmsg_add_str(m, "config_name", de->de_config_name); if(de->de_title != NULL) - htsmsg_add_str(m, "title", de->de_title); + htsmsg_add_str(m, "title", lang_str_get(de->de_title, NULL)); if(de->de_desc != NULL) - htsmsg_add_str(m, "description", de->de_desc); + htsmsg_add_str(m, "description", lang_str_get(de->de_desc, NULL)); if (de->de_bcast && de->de_bcast->episode) if (epg_episode_number_format(de->de_bcast->episode, buf, 100, NULL, "Season %d", ".", "Episode %d", "/%d")) @@ -1753,6 +1762,8 @@ extjs_config(http_connection_t *hc, const char *remain, void *opaque) pthread_mutex_lock(&global_lock); if ((str = http_arg_get(&hc->hc_req_args, "muxconfpath"))) save |= config_set_muxconfpath(str); + if ((str = http_arg_get(&hc->hc_req_args, "language"))) + save |= config_set_language(str); if (save) config_save(); pthread_mutex_unlock(&global_lock); out = htsmsg_create_map(); diff --git a/src/webui/simpleui.c b/src/webui/simpleui.c index 1dfaee00..743c72c0 100644 --- a/src/webui/simpleui.c +++ b/src/webui/simpleui.c @@ -67,6 +67,7 @@ page_simple(http_connection_t *hc, dvr_query_result_t dqr; const char *rstatus = NULL; epg_query_result_t eqr; + const char *lang = http_arg_get(&hc->hc_args, "Accept-Language"); htsbuf_qprintf(hq, ""); htsbuf_qprintf(hq, ""); @@ -86,7 +87,7 @@ page_simple(http_connection_t *hc, if(s != NULL) { - epg_query(&eqr, NULL, NULL, NULL, s); + epg_query(&eqr, NULL, NULL, NULL, s, lang); epg_query_sort(&eqr); c = eqr.eqr_entries; @@ -123,12 +124,13 @@ page_simple(http_connection_t *hc, rstatus = de != NULL ? val2str(de->de_sched_state, recstatustxt) : NULL; + s = epg_episode_get_title(e->episode, lang); htsbuf_qprintf(hq, "" "%02d:%02d-%02d:%02d %s%s%s
", e->id, a.tm_hour, a.tm_min, b.tm_hour, b.tm_min, - e->episode ? e->episode->title : "", + s ?: "", rstatus ? " " : "", rstatus ?: ""); } } @@ -199,6 +201,8 @@ page_einfo(http_connection_t *hc, const char *remain, void *opaque) dvr_entry_t *de; const char *rstatus; dvr_entry_sched_state_t dvr_status; + const char *lang = http_arg_get(&hc->hc_args, "Accept-Language"); + const char *s; pthread_mutex_lock(&global_lock); @@ -227,8 +231,9 @@ page_einfo(http_connection_t *hc, const char *remain, void *opaque) days[a.tm_wday], a.tm_mday, a.tm_mon + 1, a.tm_hour, a.tm_min, b.tm_hour, b.tm_min); + s = epg_episode_get_title(e->episode, lang); htsbuf_qprintf(hq, "
\"%s\": \"%s\"

", - e->channel->ch_name, e->episode ? e->episode->title : ""); + e->channel->ch_name, s ?: ""); dvr_status = de != NULL ? de->de_sched_state : DVR_NOSTATE; diff --git a/src/webui/static/app/config.js b/src/webui/static/app/config.js index 947f8f2d..e97f20b7 100644 --- a/src/webui/static/app/config.js +++ b/src/webui/static/app/config.js @@ -7,7 +7,7 @@ tvheadend.miscconf = function() { var confreader = new Ext.data.JsonReader( { root: 'config' }, [ - 'muxconfpath', + 'muxconfpath', 'language', ] ); @@ -21,6 +21,12 @@ tvheadend.miscconf = function() { allowBlank : true, }); + var language = new Ext.form.TextField({ + fieldLabel : 'Default Language(s)', + name : 'language', + allowBlank : true, + }); + /* **************************************************************** * Form * ***************************************************************/ @@ -53,7 +59,8 @@ tvheadend.miscconf = function() { defaultType : 'textfield', autoHeight : true, items : [ - dvbscanPath + language, + dvbscanPath, ], tbar: [ saveButton, diff --git a/src/webui/webui.c b/src/webui/webui.c index 8f6c7b3a..01d1b669 100644 --- a/src/webui/webui.c +++ b/src/webui/webui.c @@ -774,7 +774,7 @@ page_dvrfile(http_connection_t *hc, const char *remain, void *opaque) if(de->de_title != NULL) { snprintf(disposition, sizeof(disposition), - "attachment; filename=%s.%s", de->de_title, postfix); + "attachment; filename=%s.%s", lang_str_get(de->de_title, NULL), postfix); i = 20; while(disposition[i]) { if(disposition[i] == ' ')