]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
epg: query API rewrite
authorJaroslav Kysela <perex@perex.cz>
Thu, 25 Sep 2014 20:19:07 +0000 (22:19 +0200)
committerJaroslav Kysela <perex@perex.cz>
Sat, 27 Sep 2014 20:19:05 +0000 (22:19 +0200)
src/api.h
src/api/api_epg.c
src/api/api_idnode.c
src/channels.c
src/epg.c
src/epg.h
src/htsp_server.c
src/webui/extjs.c
src/webui/simpleui.c
src/webui/static/app/epg.js

index c80a2f4c56d6ae643bef2d18cebc495aef6bbc32..08e0869d43fa0c4fb1f2da9cd16565645ef1a937 100644 (file)
--- a/src/api.h
+++ b/src/api.h
@@ -25,7 +25,7 @@
 #include "redblack.h"
 #include "access.h"
 
-#define TVH_API_VERSION 14
+#define TVH_API_VERSION 15
 
 /*
  * Command hook
index d1ac6ef631391575641ec8f73167b2d99c84d1ae..1fc74b8277e069ee90ba8076d001eef05ef01121 100644 (file)
 #include "access.h"
 #include "api.h"
 #include "epg.h"
+#include "imagecache.h"
 #include "dvr/dvr.h"
 
+static htsmsg_t *
+api_epg_get_list ( const char *s )
+{
+  htsmsg_t *m = NULL;
+  char *r, *saveptr;
+  if (s && s[0] != '\0') {
+    s = r = strdup(s);
+    r = strtok_r(r, ";", &saveptr);
+    while (r) {
+      while (*r != '\0' && *r <= ' ')
+        r++;
+      if (*r != '\0') {
+        if (m == NULL)
+          m = htsmsg_create_list();
+        htsmsg_add_str(m, NULL, r);
+      }
+      r = strtok_r(NULL, ";", &saveptr);
+    }
+    free((char *)s);
+  }
+  return m;
+}
+
+static void
+api_epg_add_channel ( htsmsg_t *m, channel_t *ch )
+{
+  int64_t chnum;
+  char buf[32];
+  htsmsg_add_str(m, "channelName", channel_get_name(ch));
+  htsmsg_add_str(m, "channelUuid", channel_get_uuid(ch));
+  if ((chnum = channel_get_number(ch)) >= 0) {
+    uint32_t maj = chnum / CHANNEL_SPLIT;
+    uint32_t min = chnum % CHANNEL_SPLIT;
+    if (min)
+      snprintf(buf, sizeof(buf), "%u.%u", maj, min);
+    else
+      snprintf(buf, sizeof(buf), "%u", maj);
+    htsmsg_add_str(m, "channelNumber", buf);
+  }
+  if (ch->ch_icon)
+    htsmsg_add_imageurl(m, "chicon", "imagecache/%d", ch->ch_icon);
+}
+
 static htsmsg_t *
 api_epg_entry ( epg_broadcast_t *eb, const char *lang )
 {
@@ -31,8 +75,9 @@ api_epg_entry ( epg_broadcast_t *eb, const char *lang )
   char buf[64];
   epg_episode_t *ee = eb->episode;
   channel_t     *ch = eb->channel;
-  htsmsg_t *m;
+  htsmsg_t *m, *m2;
   epg_episode_num_t epnum;
+  epg_genre_t *eg;
   dvr_entry_t *de;
 
   if (!ee || !ch) return NULL;
@@ -40,8 +85,6 @@ api_epg_entry ( epg_broadcast_t *eb, const char *lang )
   m = htsmsg_create_map();
 
   /* EPG IDs */
-  // Note: "id" is for UI compat, remove it?
-  htsmsg_add_u32(m, "id", eb->id);
   htsmsg_add_u32(m, "eventId", eb->id);
   if (ee) {
     htsmsg_add_u32(m, "episodeId", ee->id);
@@ -55,25 +98,17 @@ api_epg_entry ( epg_broadcast_t *eb, const char *lang )
   }
   
   /* Channel Info */
-  // Note: "channel" is for UI compat, remove it?
-  htsmsg_add_str(m, "channel",     channel_get_name(ch));
-  htsmsg_add_str(m, "channelName", channel_get_name(ch));
-  htsmsg_add_str(m, "channelUuid", channel_get_uuid(ch));
-  htsmsg_add_u32(m, "channelId",   channel_get_id(ch));
+  api_epg_add_channel(m, ch);
   
   /* Time */
   htsmsg_add_s64(m, "start", eb->start);
   htsmsg_add_s64(m, "stop", eb->stop);
-  htsmsg_add_s64(m, "duration", eb->stop - eb->start);
-  // TODO: the above can be removed
 
   /* Title/description */
   if ((s = epg_broadcast_get_title(eb, lang)))
     htsmsg_add_str(m, "title", s);
-#if TODO
   if ((s = epg_broadcast_get_subtitle(eb, lang)))
     htsmsg_add_str(m, "subtitle", s);
-#endif
   if ((s = epg_broadcast_get_summary(eb, lang)))
     htsmsg_add_str(m, "summary", s);
   if ((s = epg_broadcast_get_description(eb, lang)))
@@ -108,11 +143,29 @@ api_epg_entry ( epg_broadcast_t *eb, const char *lang )
     /* Image */
     if (ee->image)
       htsmsg_add_str(m, "image", ee->image);
+
+    /* Rating */
+    if (ee->star_rating)
+      htsmsg_add_u32(m, "starRating", ee->star_rating);
+    if (ee->age_rating)
+      htsmsg_add_u32(m, "ageRating", ee->age_rating);
+
+    /* Content Type */
+    m2 = NULL;
+    LIST_FOREACH(eg, &ee->genre, link) {
+      if (m2 == NULL)
+        m2 = htsmsg_create_list();
+      htsmsg_add_u32(m2, NULL, eg->code);
+    }
+    if (m2)
+      htsmsg_add_msg(m, "genre", m2);
   }
-    
+
   /* Recording */
-  if ((de = dvr_entry_find_by_event(eb)))
-    htsmsg_add_str(m, "dvrId", idnode_uuid_as_str(&de->de_id));
+  if ((de = dvr_entry_find_by_event(eb))) {
+    htsmsg_add_str(m, "dvrUuid", idnode_uuid_as_str(&de->de_id));
+    htsmsg_add_str(m, "dvrState", dvr_entry_schedstatus(de));
+  }
 
   /* Next event */
   if ((eb = epg_broadcast_get_next(eb)))
@@ -121,30 +174,245 @@ api_epg_entry ( epg_broadcast_t *eb, const char *lang )
   return m;
 }
 
+static void
+api_epg_filter_set_str
+  ( epg_filter_str_t *f, const char *str, int comp )
+{
+  f->str = strdup(str);
+  f->comp = comp;
+}
+
+static void
+api_epg_filter_add_str
+  ( epg_query_t *eq, const char *k, const char *v, int comp )
+{
+  if (!strcmp(k, "channelName"))
+    api_epg_filter_set_str(&eq->channel_name, v, comp);
+  else if (!strcmp(k, "title"))
+    api_epg_filter_set_str(&eq->title, v, comp);
+  else if (!strcmp(k, "subtitle"))
+    api_epg_filter_set_str(&eq->subtitle, v, comp);
+  else if (!strcmp(k, "summary"))
+    api_epg_filter_set_str(&eq->summary, v, comp);
+  else if (!strcmp(k, "description"))
+    api_epg_filter_set_str(&eq->description, v, comp);
+}
+
+static void
+api_epg_filter_set_num
+  ( epg_filter_num_t *f, int64_t v1, int64_t v2, int comp )
+{
+  /* Range? */
+  if (f->comp == EC_LT && comp == EC_GT) {
+    f->val2 = f->val1;
+    f->val1 = v1;
+    f->comp = EC_RG;
+    return;
+  }
+  if (f->comp == EC_GT && comp == EC_LT) {
+    f->val2 = v1;
+    f->comp = EC_RG;
+    return;
+  }
+  f->val1 = v1;
+  f->val2 = v2;
+  f->comp = comp;
+}
+
+static void
+api_epg_filter_add_num
+  ( epg_query_t *eq, const char *k, int64_t v1, int64_t v2, int comp )
+{
+  if (!strcmp(k, "start"))
+    api_epg_filter_set_num(&eq->start, v1, v2, comp);
+  else if (!strcmp(k, "stop"))
+    api_epg_filter_set_num(&eq->stop, v1, v2, comp);
+  else if (!strcmp(k, "duration"))
+    api_epg_filter_set_num(&eq->duration, v1, v2, comp);
+  else if (!strcmp(k, "episode"))
+    api_epg_filter_set_num(&eq->episode, v1, v2, comp);
+  else if (!strcmp(k, "stars"))
+    api_epg_filter_set_num(&eq->episode, v1, v2, comp);
+  else if (!strcmp(k, "age"))
+    api_epg_filter_set_num(&eq->age, v1, v2, comp);
+}
+
+static struct strtab sortcmptab[] = {
+  { "start",         ESK_START },
+  { "stop",          ESK_STOP },
+  { "duration",      ESK_DURATION },
+  { "title",         ESK_TITLE },
+  { "subtitle",      ESK_SUBTITLE },
+  { "summary",       ESK_SUMMARY },
+  { "description",   ESK_DESCRIPTION },
+  { "channelName",   ESK_CHANNEL },
+  { "channelNumber", ESK_CHANNEL_NUM },
+  { "starRating",    ESK_STARS },
+  { "ageRating",     ESK_AGE },
+  { "genre",         ESK_GENRE }
+};
+
+static struct strtab filtcmptab[] = {
+  { "gt",    EC_GT },
+  { "lt",    EC_LT },
+  { "eq",    EC_EQ },
+  { "regex", EC_RE },
+  { "range", EC_RG }
+};
+
+static int64_t
+api_epg_decode_channel_num ( const char *s )
+{
+   int64_t v = atol(s);
+   const char *s1 = strchr(s, '.');
+   if (s1)
+     v += atol(s1 + 1) % CHANNEL_SPLIT;
+   return v;
+}
+
 static int
 api_epg_grid
   ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
 {
   int i;
-  epg_query_result_t eqr;
-  const char *ch, *tag, *title, *lang/*, *genre*/;
-  uint32_t start, limit, end;
-  htsmsg_t *l = NULL, *e;
-  int min_duration;
-  int max_duration;
+  epg_query_t eq;
+  const char *lang, *str;
+  uint32_t start, limit, end, genre;
+  int64_t duration_min, duration_max;
+  htsmsg_field_t *f, *f2;
+  htsmsg_t *l = NULL, *e, *filter;
 
   *resp = htsmsg_create_map();
 
-  /* Query params */
-  ch    = htsmsg_get_str(args, "channel");
-  tag   = htsmsg_get_str(args, "tag");
-  //genre = htsmsg_get_str(args, "genre");
-  title = htsmsg_get_str(args, "title");
-  lang  = htsmsg_get_str(args, "lang");
-  // TODO: support multiple tag/genre/channel?
+  memset(&eq, 0, sizeof(eq));
+
+  lang = htsmsg_get_str(args, "lang");
+  eq.lang = lang ? strdup(lang) : NULL;
+
+  str = htsmsg_get_str(args, "title");
+  if (str)
+    eq.stitle = strdup(str);
+  str = htsmsg_get_str(args, "channel");
+  if (str)
+    eq.channel = strdup(str);
+  str = htsmsg_get_str(args, "channelTag");
+  if (str)
+    eq.channel_tag = strdup(str);
+
+  duration_min = -1;
+  duration_max = -1;
+  htsmsg_get_s64(args, "durationMin", &duration_min);
+  htsmsg_get_s64(args, "durationMax", &duration_max);
+  if (duration_min > 0 || duration_max > 0) {
+    eq.duration.comp = EC_RG;
+    eq.duration.val1 = duration_min < 0 ? 0 : duration_min;
+    eq.duration.val2 = duration_max < 0 ? 0 : duration_max;
+  }
+
+  if (!htsmsg_get_u32(args, "contentType", &genre)) {
+    eq.genre = eq.genre_static;
+    eq.genre[0] = genre;
+    eq.genre_count = 1;
+  }
+
+  /* Filter params */
+  if ((filter = htsmsg_get_list(args, "filter"))) {
+    HTSMSG_FOREACH(f, filter) {
+      const char *k, *t, *v;
+      int comp;
+      if (!(e = htsmsg_get_map_by_field(f))) continue;
+      if (!(k = htsmsg_get_str(e, "field"))) continue;
+      if (!(t = htsmsg_get_str(e, "type")))  continue;
+      comp = str2val(htsmsg_get_str(e, "comparison") ?: "", filtcmptab);
+      if (comp == -1) comp = EC_EQ;
+      if (!strcmp(k, "channelNumber")) {
+        if (!strcmp(t, "numeric")) {
+          f2 = htsmsg_field_find(e, "value");
+          if (f2) {
+            int64_t v1, v2 = 0;
+            if (f2->hmf_type == HMF_STR) {
+              const char *s = htsmsg_field_get_str(f2);
+              const char *z = s ? strchr(s, ';') : NULL;
+              if (s) {
+                v1 = api_epg_decode_channel_num(s);
+                if (z)
+                  v2 = api_epg_decode_channel_num(z);
+                api_epg_filter_set_num(&eq.channel_num, v1, v2, comp);
+              }
+            } else {
+              if (!htsmsg_field_get_s64(f2, &v1)) {
+                if (v1 < CHANNEL_SPLIT)
+                  v1 *= CHANNEL_SPLIT;
+                api_epg_filter_set_num(&eq.channel_num, v1, 0, comp);
+              }
+            }
+          }
+        }
+      } else if (!strcmp(k, "genre")) {
+        if (!strcmp(t, "numeric")) {
+          f2 = htsmsg_field_find(e, "value");
+          if (f2) {
+            int64_t v;
+            if (f2->hmf_type == HMF_STR) {
+              htsmsg_t *z = api_epg_get_list(htsmsg_field_get_str(f2));
+              if (z) {
+                htsmsg_field_t *f3;
+                uint32_t count = 0;
+                HTSMSG_FOREACH(f3, z)
+                  count++;
+                if (ARRAY_SIZE(eq.genre_static) > count)
+                  eq.genre = malloc(sizeof(eq.genre[0]) * count);
+                else
+                  eq.genre = eq.genre_static;
+                HTSMSG_FOREACH(f3, z)
+                  if (!htsmsg_field_get_s64(f3, &v))
+                    eq.genre[eq.genre_count++] = v;
+              }
+            } else {
+              if (!htsmsg_field_get_s64(f2, &v)) {
+                eq.genre_count = 1;
+                eq.genre = eq.genre_static;
+                eq.genre[0] = v;
+              }
+            }
+          }
+        }
+      } else if (!strcmp(t, "string")) {
+        if ((v = htsmsg_get_str(e, "value")))
+          api_epg_filter_add_str(&eq, k, v, EC_RE);
+      } else if (!strcmp(t, "numeric")) {
+        f2 = htsmsg_field_find(e, "value");
+        if (f2) {
+          int64_t v1, v2 = 0;
+          if (f2->hmf_type == HMF_STR) {
+            const char *z = htsmsg_field_get_str(f2);
+            if (z) {
+              const char *z2 = strchr(z, ';');
+              if (z2)
+                v2 = strtoll(z2 + 1, NULL, 0);
+            }
+            v1 = strtoll(z, NULL, 0);
+            api_epg_filter_add_num(&eq, k, v1, v2, comp);
+          } else {
+            if (!htsmsg_field_get_s64(f2, &v1))
+              api_epg_filter_add_num(&eq, k, v1, v2, comp);
+          }
+        }
+      }
+    }
+  }
 
-  min_duration = htsmsg_get_u32_or_default(args, "minduration", 0);
-  max_duration = htsmsg_get_u32_or_default(args, "maxduration", INT_MAX);
+  /* Sort */
+  if ((str = htsmsg_get_str(args, "sort"))) {
+    int skey = str2val(str, sortcmptab);
+    if (skey >= 0) {
+      eq.sort_key = skey;
+      if ((str = htsmsg_get_str(args, "dir")) && !strcasecmp(str, "DESC"))
+        eq.sort_dir = IS_DSC;
+      else
+        eq.sort_dir = IS_ASC;
+    }
+  } /* else.. keep default start time ascending sorting */
 
   /* Pagination settings */
   start = htsmsg_get_u32_or_default(args, "start", 0);
@@ -152,27 +420,124 @@ api_epg_grid
 
   /* Query the EPG */
   pthread_mutex_lock(&global_lock); 
-  epg_query(&eqr, ch, tag, NULL, /*genre,*/ title, lang, min_duration, max_duration);
-  epg_query_sort(&eqr);
-  // TODO: optional sorting
+  epg_query(&eq);
 
   /* Build response */
-  start = MIN(eqr.eqr_entries, start);
-  end   = MIN(eqr.eqr_entries, start + limit);
+  start = MIN(eq.entries, start);
+  end   = MIN(eq.entries, start + limit);
+  l     = htsmsg_create_list();
   for (i = start; i < end; i++) {
-    if (!(e = api_epg_entry(eqr.eqr_array[i], lang))) continue;
-    if (!l) l = htsmsg_create_list();
+    if (!(e = api_epg_entry(eq.result[i], lang))) continue;
     htsmsg_add_msg(l, NULL, e);
   }
+  pthread_mutex_unlock(&global_lock);
+
+  epg_query_free(&eq);
+
+  /* Build response */
+  *resp = htsmsg_create_map();
+  htsmsg_add_u32(*resp, "totalCount", eq.entries);
+  htsmsg_add_msg(*resp, "entries", l);
+
+  return 0;
+}
+
+static void
+api_epg_episode_broadcasts
+  ( htsmsg_t *l, const char *lang, epg_episode_t *ep,
+    uint32_t *entries, epg_broadcast_t *ebc_skip )
+{
+  epg_broadcast_t *ebc;
+  channel_t *ch;
+  htsmsg_t *m;
+
+  LIST_FOREACH(ebc, &ep->broadcasts, ep_link) {
+    ch = ebc->channel;
+    if (ch == NULL) continue;
+    if (ebc == ebc_skip) continue;
+    m = api_epg_entry(ebc, lang);
+    htsmsg_add_msg(l, NULL, m);
+    (*entries)++;
+  }
+}
+
+static int
+api_epg_alternative
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+{
+  uint32_t id, entries = 0;
+  htsmsg_t *l = htsmsg_create_list();
+  epg_broadcast_t *e;
+  const char *lang = htsmsg_get_str(args, "lang");
 
+  if (!htsmsg_get_u32(args, "eventId", &id))
+    return -EINVAL;
+
+  /* Main Job */
+  pthread_mutex_lock(&global_lock);
+  e = epg_broadcast_find_by_id(id, NULL);
+  if (e && e->episode)
+    api_epg_episode_broadcasts(l, lang, e->episode, &entries, e);
   pthread_mutex_unlock(&global_lock);
 
   /* Build response */
-  htsmsg_add_u32(*resp, "totalCount", eqr.eqr_entries);
-  if (l)
-    htsmsg_add_msg(*resp, "events", l);
+  htsmsg_add_u32(*resp, "totalCount", entries);
+  htsmsg_add_msg(*resp, "entries", l);
+
+  return 0;
+}
 
+static int
+api_epg_related
+  ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
+{
+  uint32_t id, entries = 0;
+  htsmsg_t *l = htsmsg_create_list();
+  epg_broadcast_t *e;
+  epg_episode_t *ep, *ep2;
+  const char *lang = htsmsg_get_str(args, "lang");
   
+  if (!htsmsg_get_u32(args, "eventId", &id))
+    return -EINVAL;
+
+  /* Main Job */
+  pthread_mutex_lock(&global_lock);
+  e = epg_broadcast_find_by_id(id, NULL);
+  ep = e ? e->episode : NULL;
+  if (ep && ep->brand) {
+    LIST_FOREACH(ep2, &ep->brand->episodes, blink) {
+      if (ep2 == ep) continue;
+      if (!ep2->title) continue;
+      api_epg_episode_broadcasts(l, lang, ep2, &entries, e);
+      entries++;
+    }
+  } else if (ep && ep->season) {
+    LIST_FOREACH(ep2, &ep->season->episodes, slink) {
+      if (ep2 == ep) continue;
+      if (!ep2->title) continue;
+      api_epg_episode_broadcasts(l, lang, ep2, &entries, e);
+    }
+  }
+  pthread_mutex_unlock(&global_lock);
+
+  /* Build response */
+  htsmsg_add_u32(*resp, "totalCount", entries);
+  htsmsg_add_msg(*resp, "entries", l);
+
+  return 0;
+}
+
+static int
+api_epg_brand_list(access_t *perm, void *opaque, const char *op,
+                   htsmsg_t *args, htsmsg_t **resp)
+{
+  htsmsg_t *array;
+
+  *resp = htsmsg_create_map();
+  pthread_mutex_lock(&global_lock);
+  array = epg_brand_list();
+  pthread_mutex_unlock(&global_lock);
+  htsmsg_add_msg(*resp, "entries", array);
   return 0;
 }
 
@@ -181,19 +546,24 @@ api_epg_content_type_list(access_t *perm, void *opaque, const char *op,
                           htsmsg_t *args, htsmsg_t **resp)
 {
   htsmsg_t *array;
+  int full = 0;
+
+  htsmsg_get_bool(args, "full", &full);
 
   *resp = htsmsg_create_map();
-  array = epg_genres_list_all(1, 0);
+  array = epg_genres_list_all(full ? 0 : 1, 0);
   htsmsg_add_msg(*resp, "entries", array);
   return 0;
 }
 
-
 void api_epg_init ( void )
 {
   static api_hook_t ah[] = {
-    { "epg/data/grid",         ACCESS_ANONYMOUS, api_epg_grid, NULL },
-    { "epg/content_type/list", ACCESS_ANONYMOUS, api_epg_content_type_list, NULL },
+    { "epg/events/grid",        ACCESS_ANONYMOUS, api_epg_grid, NULL },
+    { "epg/events/alternative", ACCESS_ANONYMOUS, api_epg_alternative, NULL },
+    { "epg/events/related",     ACCESS_ANONYMOUS, api_epg_related, NULL },
+    { "epg/brand/list",         ACCESS_ANONYMOUS, api_epg_brand_list, NULL },
+    { "epg/content_type/list",  ACCESS_ANONYMOUS, api_epg_content_type_list, NULL },
 
     { NULL },
   };
index d3330f9c115f0657117e8cb7edc543259904ade8..abfed290c395a3bbef4f919a3d556182ccba3240 100644 (file)
@@ -101,7 +101,7 @@ api_idnode_grid_conf
   /* Sort */
   if ((str = htsmsg_get_str(args, "sort"))) {
     conf->sort.key = str;
-    if ((str = htsmsg_get_str(args, "dir")) && !strcmp(str, "DESC"))
+    if ((str = htsmsg_get_str(args, "dir")) && !strcasecmp(str, "DESC"))
       conf->sort.dir = IS_DSC;
     else
       conf->sort.dir = IS_ASC;
index 9f23c356e75abc19256901a9e69295d654795f48..ad37629bd6e5384590050074e09ec208ed81dff9 100644 (file)
@@ -386,6 +386,8 @@ channel_t *
 channel_find_by_name ( const char *name )
 {
   channel_t *ch;
+  if (name == NULL)
+    return NULL;
   CHANNEL_FOREACH(ch)
     if (!strcmp(channel_get_name(ch), name))
       break;
@@ -931,6 +933,9 @@ channel_tag_find_by_name(const char *name, int create)
 {
   channel_tag_t *ct;
 
+  if (name == NULL)
+    return NULL;
+
   TAILQ_FOREACH(ct, &channel_tags, ct_link)
     if(!strcasecmp(ct->ct_name, name))
       return ct;
index aa9ce659ef7317538fcd0352ff777126f22cff43..19d6ca547d928ad42228b909b78d1c28882481d5 100644 (file)
--- a/src/epg.c
+++ b/src/epg.c
@@ -1752,6 +1752,12 @@ const char *epg_broadcast_get_title ( epg_broadcast_t *b, const char *lang )
   return epg_episode_get_title(b->episode, lang);
 }
 
+const char *epg_broadcast_get_subtitle ( epg_broadcast_t *b, const char *lang )
+{
+  if (!b || !b->episode) return NULL;
+  return epg_episode_get_subtitle(b->episode, lang);
+}
+
 const char *epg_broadcast_get_summary ( epg_broadcast_t *b, const char *lang )
 {
   if (!b || !b->summary) return NULL;
@@ -2190,7 +2196,7 @@ htsmsg_t *epg_genres_list_all ( int major_only, int major_prefix )
     for (j = 0; j < (major_only ? 1 : 16); j++) {
       if (_epg_genre_names[i][j]) {
         e = htsmsg_create_map();
-        htsmsg_add_u32(e, "key", major_only ? i : (i << 4 | j));
+        htsmsg_add_u32(e, "key", (i << 4) | (major_only ? 0 : j));
         htsmsg_add_str(e, "val", _epg_genre_names[i][j]);
         // TODO: use major_prefix
         htsmsg_add_msg(m, NULL, e);
@@ -2204,109 +2210,393 @@ htsmsg_t *epg_genres_list_all ( int major_only, int major_prefix )
  * Querying
  * *************************************************************************/
 
-static void _eqr_add 
-  ( epg_query_result_t *eqr, epg_broadcast_t *e,
-    epg_genre_t *genre, regex_t *preg, time_t start, const char *lang, int min_duration, int max_duration )
-{
-  const char *title;
-  double duration;
-
-  /* Ignore */
-  if ( e->stop < start ) 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, title, 0, NULL, 0)) return;
-
-  duration = difftime(e->stop,e->start);
-  if ( duration < min_duration || duration > max_duration ) return;
+static inline int
+_eq_comp_num ( epg_filter_num_t *f, int64_t val )
+{
+  switch (f->comp) {
+    case EC_EQ: return val != f->val1;
+    case EC_LT: return val > f->val1;
+    case EC_GT: return val < f->val1;
+    case EC_RG: return val < f->val1 || val > f->val2;
+    default: return 0;
+  }
+}
+
+static inline int
+_eq_comp_str ( epg_filter_str_t *f, const char *str )
+{
+  switch (f->comp) {
+    case EC_EQ: return strcmp(str, f->str);
+    case EC_LT: return strcmp(str, f->str) > 0;
+    case EC_GT: return strcmp(str, f->str) < 0;
+    case EC_IN: return strstr(str, f->str) != NULL;
+    case EC_RE: return regexec(&f->re, str, 0, NULL, 0) != 0;
+    default: return 0;
+  }
+}
+
+static void
+_eq_add ( epg_query_t *eq, epg_broadcast_t *e )
+{
+  const char *s, *lang = eq->lang;
+  epg_episode_t *ep;
+
+  /* Filtering */
+  if (e->stop < dispatch_clock) return;
+  if (_eq_comp_num(&eq->start, e->start)) return;
+  if (_eq_comp_num(&eq->stop, e->stop)) return;
+  if (eq->duration.comp != EC_NO) {
+    int64_t duration = (int64_t)e->stop - (int64_t)e->start;
+    if (_eq_comp_num(&eq->duration, duration)) return;
+  }
+  ep = e->episode;
+  if (eq->stars.comp != EC_NO) {
+    if (e == NULL) return;
+    if (_eq_comp_num(&eq->stars, ep->star_rating)) return;
+  }
+  if (eq->age.comp != EC_NO) {
+    if (e == NULL) return;
+    if (_eq_comp_num(&eq->age, ep->age_rating)) return;
+  }
+  if (eq->channel_num.comp != EC_NO)
+    if (_eq_comp_num(&eq->channel_num, channel_get_number(e->channel))) return;
+  if (eq->channel_name.comp != EC_NO)
+    if (_eq_comp_str(&eq->channel_name, channel_get_name(e->channel))) return;
+  if (eq->genre_count) {
+    epg_genre_t genre;
+    uint32_t i, r = 0;
+    for (i = 0; i < eq->genre_count; i++) {
+      genre.code = eq->genre[i];
+      if (genre.code == 0) continue;
+      if (epg_genre_list_contains(&e->episode->genre, &genre, 1)) r++;
+    }
+    if (!r) return;
+  }
+  if (eq->title.comp != EC_NO || eq->stitle) {
+    if ((s = epg_episode_get_title(ep, lang)) == NULL) return;
+    if (eq->stitle)
+      if (regexec(&eq->stitle_re, s, 0, NULL, 0)) return;
+    if (_eq_comp_str(&eq->title, s)) return;
+  }
+  if (eq->subtitle.comp != EC_NO) {
+    if ((s = epg_episode_get_subtitle(ep, lang)) == NULL) return;
+    if (_eq_comp_str(&eq->subtitle, s)) return;
+  }
+  if (eq->summary.comp != EC_NO) {
+    if ((s = epg_episode_get_summary(ep, lang)) == NULL) return;
+    if (_eq_comp_str(&eq->summary, s)) return;
+  }
+  if (eq->description.comp != EC_NO) {
+    if ((s = epg_episode_get_description(ep, lang)) == NULL) return;
+    if (_eq_comp_str(&eq->description, s)) return;
+  }
 
   /* More space */
-  if ( eqr->eqr_entries == eqr->eqr_alloced ) {
-    eqr->eqr_alloced = MAX(100, eqr->eqr_alloced * 2);
-    eqr->eqr_array   = realloc(eqr->eqr_array, 
-                               eqr->eqr_alloced * sizeof(epg_broadcast_t));
+  if (eq->entries == eq->allocated) {
+    eq->allocated = MAX(100, eq->allocated + 100);
+    eq->result    = realloc(eq->result, eq->allocated * sizeof(epg_broadcast_t *));
   }
-  
+
   /* Store */
-  eqr->eqr_array[eqr->eqr_entries++] = e;
+  eq->result[eq->entries++] = e;
 }
 
-static void _eqr_add_channel 
-  ( epg_query_result_t *eqr, channel_t *ch, epg_genre_t *genre,
-    regex_t *preg, time_t start, const char *lang, int min_duration, int max_duration )
+static void
+_eq_add_channel ( epg_query_t *eq, channel_t *ch )
 {
   epg_broadcast_t *ebc;
   RB_FOREACH(ebc, &ch->ch_epg_schedule, sched_link) {
-    if ( ebc->episode ) _eqr_add(eqr, ebc, genre, preg, start, lang, min_duration, max_duration);
+    if (ebc->episode)
+      _eq_add(eq, ebc);
   }
 }
 
-void epg_query0
-  ( epg_query_result_t *eqr, channel_t *channel, channel_tag_t *tag,
-    epg_genre_t *genre, const char *title, const char *lang, int min_duration, int max_duration )
+static int
+_eq_init_str( epg_filter_str_t *f )
 {
-  time_t now;
-  channel_tag_mapping_t *ctm;
-  regex_t preg0, *preg;
-  time(&now);
+  if (f->comp != EC_RE) return 0;
+  return regcomp(&f->re, f->str, REG_ICASE | REG_EXTENDED | REG_NOSUB);
+}
 
-  /* Clear (just incase) */
-  memset(eqr, 0, sizeof(epg_query_result_t));
+static void
+_eq_done_str( epg_filter_str_t *f )
+{
+  if (f->comp == EC_RE)
+    regfree(&f->re);
+  free(f->str);
+  f->str = NULL;
+}
 
-  /* Setup exp */
-  if ( title ) {
-    if (regcomp(&preg0, title, REG_ICASE | REG_EXTENDED | REG_NOSUB) )
-      return;
-    preg = &preg0;
-  } else {
-    preg = NULL;
+static int _epg_sort_start_ascending ( const void *a, const void *b, void *eq )
+{
+  return (*(epg_broadcast_t**)a)->start - (*(epg_broadcast_t**)b)->start;
+}
+
+static int _epg_sort_start_descending ( const void *a, const void *b, void *eq )
+{
+  return (*(epg_broadcast_t**)b)->start - (*(epg_broadcast_t**)a)->start;
+}
+
+static int _epg_sort_stop_ascending ( const void *a, const void *b, void *eq )
+{
+  return (*(epg_broadcast_t**)a)->stop - (*(epg_broadcast_t**)b)->stop;
+}
+
+static int _epg_sort_stop_descending ( const void *a, const void *b, void *eq )
+{
+  return (*(epg_broadcast_t**)b)->stop - (*(epg_broadcast_t**)a)->stop;
+}
+
+static inline int64_t _epg_sort_duration( const epg_broadcast_t *b )
+{
+  return b->stop - b->start;
+}
+
+static int _epg_sort_duration_ascending ( const void *a, const void *b, void *eq )
+{
+  return _epg_sort_duration(*(epg_broadcast_t**)a) - _epg_sort_duration(*(epg_broadcast_t**)b);
+}
+
+static int _epg_sort_duration_descending ( const void *a, const void *b, void *eq )
+{
+  return _epg_sort_duration(*(epg_broadcast_t**)b) - _epg_sort_duration(*(epg_broadcast_t**)a);
+}
+
+static int _epg_sort_title_ascending ( const void *a, const void *b, void *eq )
+{
+  const char *s1 = epg_broadcast_get_title(*(epg_broadcast_t**)a, ((epg_query_t *)eq)->lang);
+  const char *s2 = epg_broadcast_get_title(*(epg_broadcast_t**)b, ((epg_query_t *)eq)->lang);
+  if (s1 == NULL && s2) return 1;
+  if (s1 && s2 == NULL) return -1;
+  return strcmp(s1, s2);
+}
+
+static int _epg_sort_title_descending ( const void *a, const void *b, void *eq )
+{
+  return _epg_sort_title_ascending(a, b, eq) * -1;
+}
+
+static int _epg_sort_subtitle_ascending ( const void *a, const void *b, void *eq )
+{
+  const char *s1 = epg_broadcast_get_subtitle(*(epg_broadcast_t**)a, ((epg_query_t *)eq)->lang);
+  const char *s2 = epg_broadcast_get_subtitle(*(epg_broadcast_t**)b, ((epg_query_t *)eq)->lang);
+  if (s1 == NULL && s2 == NULL) return 0;
+  if (s1 == NULL && s2) return 1;
+  if (s1 && s2 == NULL) return -1;
+  return strcmp(s1, s2);
+}
+
+static int _epg_sort_subtitle_descending ( const void *a, const void *b, void *eq )
+{
+  return _epg_sort_subtitle_ascending(a, b, eq) * -1;
+}
+
+static int _epg_sort_summary_ascending ( const void *a, const void *b, void *eq )
+{
+  const char *s1 = epg_broadcast_get_summary(*(epg_broadcast_t**)a, ((epg_query_t *)eq)->lang);
+  const char *s2 = epg_broadcast_get_summary(*(epg_broadcast_t**)b, ((epg_query_t *)eq)->lang);
+  if (s1 == NULL && s2 == NULL) return 0;
+  if (s1 == NULL && s2) return 1;
+  if (s1 && s2 == NULL) return -1;
+  return strcmp(s1, s2);
+}
+
+static int _epg_sort_summary_descending ( const void *a, const void *b, void *eq )
+{
+  return _epg_sort_summary_ascending(a, b, eq) * -1;
+}
+
+static int _epg_sort_description_ascending ( const void *a, const void *b, void *eq )
+{
+  const char *s1 = epg_broadcast_get_description(*(epg_broadcast_t**)a, ((epg_query_t *)eq)->lang);
+  const char *s2 = epg_broadcast_get_description(*(epg_broadcast_t**)b, ((epg_query_t *)eq)->lang);
+  if (s1 == NULL && s2 == NULL) return 0;
+  if (s1 == NULL && s2) return 1;
+  if (s1 && s2 == NULL) return -1;
+  return strcmp(s1, s2);
+}
+
+static int _epg_sort_description_descending ( const void *a, const void *b, void *eq )
+{
+  return _epg_sort_description_ascending(a, b, eq) * -1;
+}
+
+static int _epg_sort_channel_ascending ( const void *a, const void *b, void *eq )
+{
+  char *s1 = strdup(channel_get_name((*(epg_broadcast_t**)a)->channel));
+  char *s2 = strdup(channel_get_name((*(epg_broadcast_t**)b)->channel));
+  int r = strcmp(s1, s2);
+  free(s2);
+  free(s1);
+  return r;
+}
+
+static int _epg_sort_channel_descending ( const void *a, const void *b, void *eq )
+{
+  return _epg_sort_description_ascending(a, b, eq) * -1;
+}
+
+static int _epg_sort_channel_num_ascending ( const void *a, const void *b, void *eq )
+{
+  int64_t v1 = channel_get_number((*(epg_broadcast_t**)a)->channel);
+  int64_t v2 = channel_get_number((*(epg_broadcast_t**)b)->channel);
+  return v1 - v2;
+}
+
+static int _epg_sort_channel_num_descending ( const void *a, const void *b, void *eq )
+{
+  int64_t v1 = channel_get_number((*(epg_broadcast_t**)a)->channel);
+  int64_t v2 = channel_get_number((*(epg_broadcast_t**)b)->channel);
+  return v2 - v1;
+}
+
+static int _epg_sort_stars_ascending ( const void *a, const void *b, void *eq )
+{
+  return (*(epg_broadcast_t**)a)->episode->star_rating - (*(epg_broadcast_t**)b)->episode->star_rating;
+}
+
+static int _epg_sort_stars_descending ( const void *a, const void *b, void *eq )
+{
+  return (*(epg_broadcast_t**)b)->episode->star_rating - (*(epg_broadcast_t**)a)->episode->star_rating;
+}
+
+static int _epg_sort_age_ascending ( const void *a, const void *b, void *eq )
+{
+  return (*(epg_broadcast_t**)a)->episode->age_rating - (*(epg_broadcast_t**)b)->episode->age_rating;
+}
+
+static int _epg_sort_age_descending ( const void *a, const void *b, void *eq )
+{
+  return (*(epg_broadcast_t**)b)->episode->age_rating - (*(epg_broadcast_t**)a)->episode->age_rating;
+}
+
+static uint64_t _epg_sort_genre_hash( epg_episode_t *ep )
+{
+  uint64_t h = 0, t;
+  epg_genre_t *g;
+
+  LIST_FOREACH(g, &ep->genre, link) {
+    t = h >> 28;
+    h <<= 8;
+    h += (uint64_t)g->code + t;
   }
-  
+  return h;
+}
+
+static int _epg_sort_genre_ascending ( const void *a, const void *b, void *eq )
+{
+  uint64_t v1 = _epg_sort_genre_hash((*(epg_broadcast_t**)a)->episode);
+  uint64_t v2 = _epg_sort_genre_hash((*(epg_broadcast_t**)b)->episode);
+  return v1 - v2;
+}
+
+static int _epg_sort_genre_descending ( const void *a, const void *b, void *eq )
+{
+  return _epg_sort_genre_ascending(a, b, eq) * -1;
+}
+
+epg_broadcast_t **
+epg_query ( epg_query_t *eq )
+{
+  channel_t *channel;
+  channel_tag_t *tag;
+  int (*fcn)(const void *, const void *, void *) = NULL;
+
+  /* Setup exp */
+  if (_eq_init_str(&eq->title)) goto fin;
+  if (_eq_init_str(&eq->subtitle)) goto fin;
+  if (_eq_init_str(&eq->summary)) goto fin;
+  if (_eq_init_str(&eq->description)) goto fin;
+  if (_eq_init_str(&eq->channel_name)) goto fin;
+
+  if (eq->stitle)
+    if (regcomp(&eq->stitle_re, eq->stitle, REG_ICASE | REG_EXTENDED | REG_NOSUB))
+      goto fin;
+
+  channel = channel_find_by_uuid(eq->channel) ?:
+            channel_find_by_name(eq->channel);
+
+  tag = channel_tag_find_by_uuid(eq->channel_tag) ?:
+        channel_tag_find_by_name(eq->channel_tag, 0);
+
   /* Single channel */
-  if (channel && !tag) {
-    _eqr_add_channel(eqr, channel, genre, preg, now, lang, min_duration, max_duration);
+  if (channel && tag == NULL) {
+    _eq_add_channel(eq, channel);
   
   /* Tag based */
-  } else if ( tag ) {
+  } else if (tag) {
+    channel_tag_mapping_t *ctm;
     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, lang, min_duration, max_duration);
+        _eq_add_channel(eq, ctm->ctm_channel);
     }
 
   /* All channels */
   } else {
     CHANNEL_FOREACH(channel)
-      _eqr_add_channel(eqr, channel, genre, preg, now, lang, min_duration, max_duration);
+      _eq_add_channel(eq, channel);
+  }
+
+  switch (eq->sort_dir) {
+  case ES_ASC:
+    switch (eq->sort_key) {
+    case ESK_START:       fcn = _epg_sort_start_ascending;        break;
+    case ESK_STOP:        fcn = _epg_sort_stop_ascending;         break;
+    case ESK_DURATION:    fcn = _epg_sort_duration_ascending;     break;
+    case ESK_TITLE:       fcn = _epg_sort_title_ascending;        break;
+    case ESK_SUBTITLE:    fcn = _epg_sort_subtitle_ascending;     break;
+    case ESK_SUMMARY:     fcn = _epg_sort_summary_ascending;      break;
+    case ESK_DESCRIPTION: fcn = _epg_sort_description_ascending;  break;
+    case ESK_CHANNEL:     fcn = _epg_sort_channel_ascending;      break;
+    case ESK_CHANNEL_NUM: fcn = _epg_sort_channel_num_ascending;  break;
+    case ESK_STARS:       fcn = _epg_sort_stars_ascending;        break;
+    case ESK_AGE:         fcn = _epg_sort_age_ascending;          break;
+    case ESK_GENRE:       fcn = _epg_sort_genre_ascending;        break;
+    }
+    break;
+  case ES_DSC:
+    switch (eq->sort_key) {
+    case ESK_START:       fcn = _epg_sort_start_descending;       break;
+    case ESK_STOP:        fcn = _epg_sort_stop_descending;        break;
+    case ESK_DURATION:    fcn = _epg_sort_duration_descending;    break;
+    case ESK_TITLE:       fcn = _epg_sort_title_descending;       break;
+    case ESK_SUBTITLE:    fcn = _epg_sort_subtitle_descending;    break;
+    case ESK_SUMMARY:     fcn = _epg_sort_summary_descending;     break;
+    case ESK_DESCRIPTION: fcn = _epg_sort_description_descending; break;
+    case ESK_CHANNEL:     fcn = _epg_sort_channel_descending;     break;
+    case ESK_CHANNEL_NUM: fcn = _epg_sort_channel_num_descending; break;
+    case ESK_STARS:       fcn = _epg_sort_stars_descending;       break;
+    case ESK_AGE:         fcn = _epg_sort_age_descending;         break;
+    case ESK_GENRE:       fcn = _epg_sort_genre_descending;       break;
+    }
+    break;
   }
-  if (preg) regfree(preg);
 
-  return;
-}
+  tvh_qsort_r(eq->result, eq->entries, sizeof(epg_broadcast_t *), fcn, eq);
 
-void epg_query(epg_query_result_t *eqr, const char *channel, const char *tag,
-            epg_genre_t *genre, const char *title, const char *lang, int min_duration, int max_duration)
-{
-  channel_t     *ch = channel ? channel_find(channel)    : NULL;
-  channel_tag_t *ct = tag     ? channel_tag_find_by_uuid(tag) : NULL;
+fin:
+  _eq_done_str(&eq->title);
+  _eq_done_str(&eq->subtitle);
+  _eq_done_str(&eq->summary);
+  _eq_done_str(&eq->description);
+  _eq_done_str(&eq->channel_name);
 
-  epg_query0(eqr, ch, ct, genre, title, lang, min_duration, max_duration);
-}
+  free(eq->lang); eq->lang = NULL;
+  free(eq->channel); eq->channel = NULL;
+  free(eq->channel_tag); eq->channel_tag = NULL;
+  free(eq->stitle); eq->stitle = NULL;
+  if (eq->genre != eq->genre_static)
+    free(eq->genre);
+  eq->genre = NULL;
 
-void epg_query_free(epg_query_result_t *eqr)
-{
-  free(eqr->eqr_array);
+  return eq->result;
 }
 
-static int _epg_sort_start_ascending ( const void *a, const void *b )
+void epg_query_free(epg_query_t *eq)
 {
-  return (*(epg_broadcast_t**)a)->start - (*(epg_broadcast_t**)b)->start;
+  free(eq->result); eq->result = NULL;
 }
 
-void epg_query_sort(epg_query_result_t *eqr)
-{
-  qsort(eqr->eqr_array, eqr->eqr_entries, sizeof(epg_broadcast_t*),
-        _epg_sort_start_ascending);
-}
 
 /* **************************************************************************
  * Miscellaneous
index eb3fcb87d738d03f4bc63ecd4f49bdef317e3fe7..fd17e5da7875af4cc7cf145d12a8f47fed659a63 100644 (file)
--- a/src/epg.h
+++ b/src/epg.h
@@ -19,6 +19,7 @@
 #ifndef EPG_H
 #define EPG_H
 
+#include <regex.h>
 #include "settings.h"
 #include "lang_str.h"
 
@@ -507,6 +508,8 @@ epg_episode_t   *epg_broadcast_get_episode
   ( epg_broadcast_t *b, int create, int *save );
 const char *epg_broadcast_get_title 
   ( epg_broadcast_t *b, const char *lang );
+const char *epg_broadcast_get_subtitle
+  ( epg_broadcast_t *b, const char *lang );
 const char *epg_broadcast_get_summary
   ( epg_broadcast_t *b, const char *lang );
 const char *epg_broadcast_get_description
@@ -528,28 +531,80 @@ void epg_channel_unlink ( struct channel *ch );
  * Querying
  * ***********************************************************************/
 
-/*
- * Query result
- */
-typedef struct epg_query_result {
-  epg_broadcast_t **eqr_array;
-  int               eqr_entries;
-  int               eqr_alloced;
-} epg_query_result_t;
-
-void epg_query_free(epg_query_result_t *eqr);
-
-/* Sorting */
-// WIBNI: might be useful to have a user defined comparator function
-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,
-                const char *lang, int min_duration, int max_duration);
-void epg_query(epg_query_result_t *eqr, const char *channel, const char *tag,
-              epg_genre_t *genre, const char *title, const char *lang, int min_duration, int max_duration);
-
+typedef enum {
+  EC_NO, ///< No filter
+  EC_EQ, ///< Equals
+  EC_LT, ///< LT
+  EC_GT, ///< GT
+  EC_RG, ///< Range
+  EC_IN, ///< contains (STR only)
+  EC_RE, ///< regexp (STR only)
+} epg_comp_t;
+
+typedef struct epg_filter_str {
+  char      *str;
+  regex_t    re;
+  epg_comp_t comp;
+} epg_filter_str_t;
+
+typedef struct epg_filter_num {
+  int64_t    val1;
+  int64_t    val2;
+  epg_comp_t comp;
+} epg_filter_num_t;
+
+typedef struct epg_query {
+  /* Configuration */
+  char             *lang;
+
+  /* Filter */
+  epg_filter_num_t  start;
+  epg_filter_num_t  stop;
+  epg_filter_num_t  duration;
+  epg_filter_str_t  title;
+  epg_filter_str_t  subtitle;
+  epg_filter_str_t  summary;
+  epg_filter_str_t  description;
+  epg_filter_num_t  episode;
+  epg_filter_num_t  stars;
+  epg_filter_num_t  age;
+  epg_filter_str_t  channel_name;
+  epg_filter_num_t  channel_num;
+  char             *stitle;
+  regex_t           stitle_re;
+  char             *channel;
+  char             *channel_tag;
+  uint32_t          genre_count;
+  uint8_t          *genre;
+  uint8_t           genre_static[16];
+
+  enum {
+    ESK_START,
+    ESK_STOP,
+    ESK_DURATION,
+    ESK_TITLE,
+    ESK_SUBTITLE,
+    ESK_SUMMARY,
+    ESK_DESCRIPTION,
+    ESK_CHANNEL,
+    ESK_CHANNEL_NUM,
+    ESK_STARS,
+    ESK_AGE,
+    ESK_GENRE
+  } sort_key;
+  enum {
+    ES_ASC,
+    ES_DSC
+  } sort_dir;
+
+  /* Result */
+  epg_broadcast_t **result;
+  uint32_t          entries;
+  uint32_t          allocated;
+} epg_query_t;
+
+epg_broadcast_t  **epg_query(epg_query_t *eq);
+void epg_query_free(epg_query_t *eq);
 
 /* ************************************************************************
  * Setup/Shutdown
index b7d44bfd6540a12cf4ff8c7d14406916b3c27b40..c78b6ab28835a61e2602d5a059dd99c23ba39183 100644 (file)
@@ -1142,8 +1142,7 @@ htsp_method_epgQuery(htsp_connection_t *htsp, htsmsg_t *in)
   uint32_t u32, full;
   channel_t *ch = NULL;
   channel_tag_t *ct = NULL;
-  epg_query_result_t eqr;
-  epg_genre_t genre, *eg = NULL;
+  epg_query_t eq;
   const char *lang;
   int min_duration;
   int max_duration;
@@ -1151,48 +1150,61 @@ htsp_method_epgQuery(htsp_connection_t *htsp, htsmsg_t *in)
   /* Required */
   if( (query = htsmsg_get_str(in, "query")) == NULL )
     return htsp_error("Missing argument 'query'");
+
+  memset(&eq, 0, sizeof(eq));
   
   /* Optional */
-  if(!(htsmsg_get_u32(in, "channelId", &u32)))
+  if(!(htsmsg_get_u32(in, "channelId", &u32))) {
     if (!(ch = channel_find_by_id(u32)))
       return htsp_error("Channel does not exist");
-  if(!(htsmsg_get_u32(in, "tagId", &u32)))
+    else
+      eq.channel = strdup(idnode_uuid_as_str(&ch->ch_id));
+  }
+  if(!(htsmsg_get_u32(in, "tagId", &u32))) {
     if (!(ct = htsp_channel_tag_find_by_identifier(u32)))
       return htsp_error("Channel tag does not exist");
+    else
+      eq.channel_tag = strdup(idnode_uuid_as_str(&ct->ct_id));
+  }
   if (!htsmsg_get_u32(in, "contentType", &u32)) {
     if(htsp->htsp_version < 6) u32 <<= 4;
-    genre.code = u32;
-    eg         = &genre;
+    eq.genre_count = 1;
+    eq.genre = eq.genre_static;
+    eq.genre[0] = u32;
   }
   lang = htsmsg_get_str(in, "language") ?: htsp->htsp_language;
+  eq.lang = lang ? strdup(lang) : NULL;
   full = htsmsg_get_u32_or_default(in, "full", 0);
 
   min_duration = htsmsg_get_u32_or_default(in, "minduration", 0);
   max_duration = htsmsg_get_u32_or_default(in, "maxduration", INT_MAX);
+  eq.duration.comp = EC_RG;
+  eq.duration.val1 = min_duration;
+  eq.duration.val2 = max_duration;
   tvhtrace("htsp", "min_duration %d and max_duration %d", min_duration, max_duration);
 
   /* Check access */
   if (!htsp_user_access_channel(htsp, ch))
     return htsp_error("User does not have access");
 
-  //do the query
-  epg_query0(&eqr, ch, ct, eg, query, lang, min_duration, max_duration);
+  /* Query */
+  epg_query(&eq);
 
-  // create reply
+  /* Create Reply */
   out = htsmsg_create_map();
-  if( eqr.eqr_entries ) {
+  if( eq.entries ) {
     array = htsmsg_create_list();
-    for(i = 0; i < eqr.eqr_entries; ++i) {
+    for(i = 0; i < eq.entries; ++i) {
       if (full)
         htsmsg_add_msg(array, NULL,
-                       htsp_build_event(eqr.eqr_array[i], NULL, lang, 0, htsp));
+                       htsp_build_event(eq.result[i], NULL, lang, 0, htsp));
       else
-        htsmsg_add_u32(array, NULL, eqr.eqr_array[i]->id);
+        htsmsg_add_u32(array, NULL, eq.result[i]->id);
     }
     htsmsg_add_msg(out, full ? "events" : "eventIds", array);
   }
   
-  epg_query_free(&eqr);
+  epg_query_free(&eq);
   
   return out;
 }
index edadc6e6d777d6516496afcfba95316ac8edda7e..521102faf1201d1bce1d7aaad0dfb4b3eb86b7d7 100755 (executable)
@@ -540,253 +540,6 @@ extjs_languages(http_connection_t *hc, const char *remain, void *opaque)
 
 }
 
-/**
- *
- */
-static int
-extjs_epg(http_connection_t *hc, const char *remain, void *opaque)
-{
-  htsbuf_queue_t *hq = &hc->hc_reply;
-  htsmsg_t *out, *array, *m;
-  epg_query_result_t eqr;
-  epg_broadcast_t *e;
-  epg_episode_t *ee = NULL;
-  epg_genre_t *eg = NULL, genre;
-  channel_t *ch;
-  int start = 0, end, limit, i;
-  const char *s;
-  char buf[100];
-  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");
-
-  int min_duration;
-  int max_duration;
-
-  if(channel && !channel[0]) channel = NULL;
-  if(tag     && !tag[0])     tag = NULL;
-
-  if((s = http_arg_get(&hc->hc_req_args, "minduration")) != NULL)
-    min_duration = atoi(s);
-  else
-    min_duration = 0;
-
-  if((s = http_arg_get(&hc->hc_req_args, "maxduration")) != NULL)
-    max_duration = atoi(s);
-  else
-    max_duration = INT_MAX;
-
-  if((s = http_arg_get(&hc->hc_req_args, "start")) != NULL)
-    start = atoi(s);
-
-  if((s = http_arg_get(&hc->hc_req_args, "limit")) != NULL)
-    limit = atoi(s);
-  else
-    limit = 20; /* XXX */
-
-  if ((s = http_arg_get(&hc->hc_req_args, "content_type"))) {
-    genre.code = atoi(s) * 16;
-    eg = &genre;
-  }
-
-  out = htsmsg_create_map();
-  array = htsmsg_create_list();
-
-  pthread_mutex_lock(&global_lock);
-
-  epg_query(&eqr, channel, tag, eg, title, lang, min_duration, max_duration);
-
-  epg_query_sort(&eqr);
-
-  htsmsg_add_u32(out, "totalCount", eqr.eqr_entries);
-
-
-  start = MIN(start, eqr.eqr_entries);
-  end = MIN(start + limit, eqr.eqr_entries);
-
-  for(i = start; i < end; i++) {
-    e  = eqr.eqr_array[i];
-    ee = e->episode;
-    ch = e->channel;
-    if (!ch||!ee) continue;
-
-    m = htsmsg_create_map();
-
-    htsmsg_add_str(m, "channel", channel_get_name(ch));
-    htsmsg_add_u32(m, "channelid", channel_get_id(ch));
-    if(ch->ch_icon != NULL)
-      htsmsg_add_imageurl(m, "chicon", "imagecache/%d", ch->ch_icon);
-
-    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((s = epg_broadcast_get_description(e, lang)))
-      htsmsg_add_str(m, "description", s);
-    else if((s = epg_broadcast_get_summary(e, lang)))
-      htsmsg_add_str(m, "description", s);
-
-    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);
-    htsmsg_add_u32(m, "start", e->start);
-    htsmsg_add_u32(m, "end", e->stop);
-    htsmsg_add_u32(m, "duration", e->stop - e->start);
-    if(ee->star_rating)
-       htsmsg_add_u32(m, "starrating", ee->star_rating);
-    if(ee->age_rating)
-       htsmsg_add_u32(m, "agerating", ee->age_rating);
-
-    if(e->serieslink)
-      htsmsg_add_str(m, "serieslink", e->serieslink->uri);
-    
-    if((eg = LIST_FIRST(&ee->genre))) {
-      htsmsg_add_u32(m, "content_type", eg->code / 16);
-    }
-
-    dvr_entry_t *de;
-    if((de = dvr_entry_find_by_event(e)) != NULL)
-      htsmsg_add_str(m, "schedstate", dvr_entry_schedstatus(de));
-
-    htsmsg_add_msg(array, NULL, m);
-  }
-
-  epg_query_free(&eqr);
-
-  pthread_mutex_unlock(&global_lock);
-
-  htsmsg_add_msg(out, "entries", array);
-
-  htsmsg_json_serialize(out, hq, 0);
-  htsmsg_destroy(out);
-  http_output_content(hc, "text/x-json; charset=UTF-8");
-  return 0;
-}
-
-static int
-extjs_epgrelated(http_connection_t *hc, const char *remain, void *opaque)
-{
-  htsbuf_queue_t *hq = &hc->hc_reply;
-  htsmsg_t *out, *array, *m;
-  epg_broadcast_t *e, *ebc;
-  epg_episode_t *ee, *ee2;
-  channel_t *ch;
-  uint32_t count = 0;
-  const char *s;
-  char buf[100];
-
-  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();
-
-  pthread_mutex_lock(&global_lock);
-  if ( id && type ) {
-    e = epg_broadcast_find_by_id(atoi(id), NULL);
-    if ( e && e->episode ) {
-      ee = e->episode;
-
-      /* Alternative broadcasts */
-      if (!strcmp(type, "alternative")) {
-        LIST_FOREACH(ebc, &ee->broadcasts, ep_link) {
-          ch = ebc->channel;
-          if ( !ch ) continue; // skip something not viewable
-          if ( ebc == e ) continue; // skip self
-          count++;
-          m = htsmsg_create_map();
-          htsmsg_add_u32(m, "id", ebc->id);
-          htsmsg_add_str(m, "channel", channel_get_name(ch));
-          if (ch->ch_icon)
-            htsmsg_add_imageurl(m, "chicon", "imagecache/%d", ch->ch_icon);
-          htsmsg_add_u32(m, "start", ebc->start);
-          htsmsg_add_msg(array, NULL, m);
-        }
-      
-      /* Related */
-      } else if (!strcmp(type, "related")) {
-        if (ee->brand) {
-          LIST_FOREACH(ee2, &ee->brand->episodes, blink) {
-            if (ee2 == ee) continue;
-            if (!ee2->title) continue;
-            count++;
-            m = htsmsg_create_map();
-            htsmsg_add_str(m, "uri", ee2->uri);
-            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);
-          }
-        } else if (ee->season) {
-          LIST_FOREACH(ee2, &ee->season->episodes, slink) {
-            if (ee2 == ee) continue;
-            if (!ee2->title) continue;
-            count++;
-            m = htsmsg_create_map();
-            htsmsg_add_str(m, "uri", ee2->uri);
-            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);
-          }
-        }
-      }
-    }
-  }
-  pthread_mutex_unlock(&global_lock);
-
-  htsmsg_add_u32(out, "totalCount", count);
-  htsmsg_add_msg(out, "entries", array);
-  htsmsg_json_serialize(out, hq, 0);
-  htsmsg_destroy(out);
-  http_output_content(hc, "text/x-json; charset=UTF-8");
-  return 0;
-}
-
-/**
- *
- */
-static int
-extjs_epgobject(http_connection_t *hc, const char *remain, void *opaque)
-{
-  htsbuf_queue_t *hq = &hc->hc_reply;
-  const char *op = http_arg_get(&hc->hc_req_args, "op");
-  htsmsg_t *out, *array;
-
-  if(op == NULL)
-    return 400;
-
-  if (!strcmp(op, "brandList")) {
-    out   = htsmsg_create_map();
-    pthread_mutex_lock(&global_lock);
-    array = epg_brand_list();
-    pthread_mutex_unlock(&global_lock);
-    htsmsg_add_msg(out, "entries", array);
-
-  } else {
-    return HTTP_STATUS_BAD_REQUEST;
-  }
-
-  htsmsg_json_serialize(out, hq, 0);
-  htsmsg_destroy(out);
-  http_output_content(hc, "text/x-json; charset=UTF-8");
-
-  return 0;
-}
-
 /**
  *
  */
@@ -1062,9 +815,6 @@ extjs_start(void)
   http_path_add("/capabilities",     NULL, extjs_capabilities,     ACCESS_WEB_INTERFACE);
   http_path_add("/tablemgr",         NULL, extjs_tablemgr,         ACCESS_WEB_INTERFACE);
   http_path_add("/epggrab",          NULL, extjs_epggrab,          ACCESS_WEB_INTERFACE);
-  http_path_add("/epg",              NULL, extjs_epg,              ACCESS_WEB_INTERFACE);
-  http_path_add("/epgrelated",       NULL, extjs_epgrelated,       ACCESS_WEB_INTERFACE);
-  http_path_add("/epgobject",        NULL, extjs_epgobject,        ACCESS_WEB_INTERFACE);
   http_path_add("/config",           NULL, extjs_config,           ACCESS_WEB_INTERFACE);
   http_path_add("/languages",        NULL, extjs_languages,        ACCESS_WEB_INTERFACE);
 #if ENABLE_TIMESHIFT
index bb0ad8f6fcdecb05e70ca18fae07ccb6d7c81cef..1bf03663249a86aa4d63606b111539af460499ac 100644 (file)
@@ -164,7 +164,6 @@ page_simple(http_connection_t *hc,
   dvr_entry_t *de;
   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, "<html>");
@@ -184,14 +183,17 @@ page_simple(http_connection_t *hc,
 
 
   if(s != NULL) {
+    epg_query_t eq;
+
+    memset(&eq, 0, sizeof(eq));
+    eq.lang = strdup(lang);
 
     //Note: force min/max durations for this interface to 0 and INT_MAX seconds respectively
-    epg_query(&eqr, NULL, NULL, NULL, s, lang, 0, INT_MAX);
-    epg_query_sort(&eqr);
+    epg_query(&eq);
 
-    c = eqr.eqr_entries;
+    c = eq.entries;
 
-    if(eqr.eqr_entries == 0) {
+    if(eq.entries == 0) {
       htsbuf_qprintf(hq, "<b>No matching entries found</b>");
     } else {
 
@@ -206,7 +208,7 @@ page_simple(http_connection_t *hc,
 
       memset(&day, -1, sizeof(struct tm));
       for(k = 0; k < c; k++) {
-       e = eqr.eqr_array[k];
+       e = eq.result[k];
       
        localtime_r(&e->start, &a);
        localtime_r(&e->stop, &b);
@@ -234,7 +236,7 @@ page_simple(http_connection_t *hc,
       }
     }
     htsbuf_qprintf(hq, "<hr>");
-    epg_query_free(&eqr);
+    epg_query_free(&eq);
   }
 
 
index d0ed1546a630aba4342ef8c988b86b2cf13177d3..3118dfeb0b2c34dcc8288b0d97c9ecde51de4465 100644 (file)
@@ -1,13 +1,3 @@
-tvheadend.brands = new Ext.data.JsonStore({
-    root: 'entries',
-    fields: ['uri', 'title'],
-    autoLoad: true,
-    url: 'epgobject',
-    baseParams: {
-        op: 'brandList'
-    }
-});
-
 insertContentGroupClearOption = function( scope, records, options ){
     var placeholder = Ext.data.Record.create(['val', 'key']);
     scope.insert(0,new placeholder({val: '(Clear filter)', key: '-1'}));
@@ -22,7 +12,23 @@ tvheadend.ContentGroupStore = tvheadend.idnode_get_enum({
 
 tvheadend.contentGroupLookupName = function(code) {
     ret = "";
+    if (!code)
+        code = 0;
     tvheadend.ContentGroupStore.each(function(r) {
+        if (r.data.key === code & 0xf0)
+            ret = r.data.val;
+    });
+    return ret;
+};
+
+tvheadend.ContentGroupFullStore = tvheadend.idnode_get_enum({
+    url: 'api/epg/content_type/list',
+    params: { full: 1 }
+});
+
+tvheadend.contentGroupFullLookupName = function(code) {
+    ret = "";
+    tvheadend.ContentGroupFullStore.each(function(r) {
         if (r.data.key === code)
             ret = r.data.val;
     });
@@ -40,7 +46,7 @@ tvheadend.channelLookupName = function(key) {
     return channelString;
 };
 
-tvheadend.tagLookupName = function(key) {
+tvheadend.channelTagLookupName = function(key) {
     tagString = "";
 
     var index = tvheadend.channelTags.find('key', key);
@@ -83,38 +89,46 @@ tvheadend.epgDetails = function(event) {
 
     var content = '';
 
-    if (event.chicon != null && event.chicon.length > 0)
-        content += '<img class="x-epg-chicon" src="' + event.chicon + '">';
+    if (event.channelIcon != null && event.channelIcon.length > 0)
+        content += '<img class="x-epg-chicon" src="' + event.channelIcon + '">';
 
     content += '<div class="x-epg-title">' + event.title;
     if (event.subtitle)
         content += "&nbsp;:&nbsp;" + event.subtitle;
     content += '</div>';
-    content += '<div class="x-epg-desc">' + event.episode + '</div>';
-    content += '<div class="x-epg-desc">' + event.description + '</div>';
-    content += '<div class="x-epg-meta">' + event.starrating + '</div>';
-    content += '<div class="x-epg-meta">' + event.agerating + '</div>';
-    content += '<div class="x-epg-meta">' + tvheadend.contentGroupLookupName(event.content_type) + '</div>';
-
-    if (event.ext_desc != null)
-        content += '<div class="x-epg-meta">' + event.ext_desc + '</div>';
-
-    if (event.ext_item != null)
-        content += '<div class="x-epg-meta">' + event.ext_item + '</div>';
-
-    if (event.ext_text != null)
-        content += '<div class="x-epg-meta">' + event.ext_text + '</div>';
+    if (event.episodeOnscreen)
+        content += '<div class="x-epg-desc">' + event.episodeOnscreen + '</div>';
+    if (event.summary)
+      content += '<div class="x-epg-desc"><b>' + event.summary + '</b></div>';
+    if (event.description)
+      content += '<div class="x-epg-desc"><p>' + event.description + '</p></div>';
+    if (event.starRating)
+      content += '<div class="x-epg-meta">Star Rating: ' + event.starRating + '</div>';
+    if (event.ageRating)
+      content += '<div class="x-epg-meta">Age Rating: ' + event.ageRating + '</div>';
+    if (event.genre) {
+      var genre = [];
+      Ext.each(event.genre, function(g) {
+        var g1 = tvheadend.contentGroupLookupName(g);
+        var g2 = tvheadend.contentGroupFullLookupName(g);
+        if (g1 == g2)
+          g1 = '';
+        if (g1 || g2)
+          genre.push((g1 ? '[' + g1 + '] ' : '') + g2);
+      });
+      content += '<div class="x-epg-meta">Content Type: ' + genre.join(', ') + '</div>';
+    }
 
     content += '<div class="x-epg-meta"><a target="_blank" href="http://akas.imdb.com/find?q=' + event.title + '">Search IMDB</a></div>';
     content += '<div id="related"></div>';
     content += '<div id="altbcast"></div>';
     
     now = new Date();
-    if (event.start < now && event.end > now) {
+    if (event.start < now && event.stop > now) {
         var title = event.title;
-        if (event.episode)
-          title += ' / ' + event.episode;
-        content += '<div class="x-epg-meta"><a href="play/stream/channelid/' + event.channelid +
+        if (event.episodeOnscreen)
+          title += ' / ' + event.episodeOnscreen;
+        content += '<div class="x-epg-meta"><a href="play/stream/channel/' + event.channelUuid +
                    '?title=' + encodeURIComponent(title) + '">Play</a></div>';
     }
 
@@ -158,7 +172,7 @@ tvheadend.epgDetails = function(event) {
         }));
         buttons.push(new Ext.Button({
             handler: recordSeries,
-            text: event.serieslink ? "Record series" : "Autorec"
+            text: event.serieslinkId ? "Record series" : "Autorec"
         }));
 
     } else {
@@ -177,6 +191,7 @@ tvheadend.epgDetails = function(event) {
         constrainHeader: true,
         buttons: buttons,
         buttonAlign: 'center',
+        autoScroll: true,
         html: content
     });
     win.show();
@@ -193,7 +208,7 @@ tvheadend.epgDetails = function(event) {
         Ext.Ajax.request({
             url: url,
             params: {
-                event_id: event.id,
+                event_id: event.eventId,
                 config_uuid: confcombo.getValue()
             },
             success: function(response, options) {
@@ -212,55 +227,46 @@ tvheadend.epg = function() {
         width: 20,
         dataIndex: 'actions',
         actions: [{
-                iconIndex: 'schedstate'
+                iconIndex: 'dvrState'
             }]
     });
 
     var epgStore = new Ext.ux.grid.livegrid.Store({
         autoLoad: true,
-        url: 'epg',
+        url: 'api/epg/events/grid',
         bufferSize: 300,
         reader: new Ext.ux.grid.livegrid.JsonReader({
             root: 'entries',
             totalProperty: 'totalCount',
-            id: 'id'
-        }, [{
-                name: 'id'
-            }, {
-                name: 'channel'
-            }, {
-                name: 'channelid'
-            }, {
-                name: 'title'
-            }, {
-                name: 'subtitle'
-            }, {
-                name: 'episode'
-            }, {
-                name: 'description'
-            }, {
-                name: 'chicon'
-            }, {
+            id: 'eventId',
+        },
+        [
+            { name: 'eventId' },
+            { name: 'channelName' },
+            { name: 'channelUuid' },
+            { name: 'channelNumber' },
+            { name: 'channelIcon' },
+            { name: 'title' },
+            { name: 'subtitle' },
+            { name: 'summary' },
+            { name: 'description' },
+            { name: 'episodeOnscreen' },
+            {
                 name: 'start',
                 type: 'date',
                 dateFormat: 'U' /* unix time */
-            }, {
-                name: 'end',
+            },
+            {
+                name: 'stop',
                 type: 'date',
                 dateFormat: 'U' /* unix time */
-            }, {
-                name: 'duration'
-            }, {
-                name: 'starrating'
-            }, {
-                name: 'agerating'
-            }, {
-                name: 'content_type'
-            }, {
-                name: 'schedstate'
-            }, {
-                name: 'serieslink'
-            }])
+            },
+            { name: 'starRating' },
+           { name: 'ageRating' },
+            { name: 'genre' },
+            { name: 'dvrState' },
+            { name: 'serieslinkId' },
+        ]),
     });
 
     function setMetaAttr(meta, record) {
@@ -275,14 +281,21 @@ tvheadend.epg = function() {
     function renderDate(value, meta, record, rowIndex, colIndex, store) {
         setMetaAttr(meta, record);
 
-        var dt = new Date(value);
-        return dt.format('D, M d, H:i');
+        if (value) {
+          var dt = new Date(value);
+          return dt.format('D, M d, H:i');
+        }
+        return "";
     }
 
     function renderDuration(value, meta, record, rowIndex, colIndex, store) {
         setMetaAttr(meta, record);
 
-        value = Math.floor(value / 60);
+        value = record.data.stop - record.data.start;
+        if (!value || value < 0)
+            value = 0;
+
+        value = Math.floor(value / 60000);
 
         if (value >= 60) {
             var min = value % 60;
@@ -310,90 +323,131 @@ tvheadend.epg = function() {
         return '' + value;
     }
 
-    var epgCm = new Ext.grid.ColumnModel([actions, 
-        new Ext.ux.grid.ProgressColumn({
-            width: 100,
-            header: "Progress",
-            dataIndex: 'progress',
-            colored: false,
-            ceiling: 100,
-            tvh_renderer: function(value, meta, record, rowIndex, colIndex, store) {
-                var entry = record.data;
-                var start = entry.start;
-                var end = entry.end;
-                var duration = entry.duration; // seconds
-                var now = new Date();
-
-                // Only render a progress bar for currently running programmes
-                if (now >= start)
-                    return (now - start) / 1000 / duration * 100;
-                else
-                    return "";
-            }
-        }), {
-            width: 250,
-            id: 'title',
-            header: "Title",
-            dataIndex: 'title',
-            renderer: renderText
-        }, {
-            width: 250,
-            id: 'subtitle',
-            header: "SubTitle",
-            dataIndex: 'subtitle',
-            renderer: renderText
-        }, {
-            width: 100,
-            id: 'episode',
-            header: "Episode",
-            dataIndex: 'episode',
-            renderer: renderText
-        }, {
-            width: 100,
-            id: 'start',
-            header: "Start",
-            dataIndex: 'start',
-            renderer: renderDate
-        }, {
-            width: 100,
-            hidden: true,
-            id: 'end',
-            header: "End",
-            dataIndex: 'end',
-            renderer: renderDate
-        }, {
-            width: 100,
-            id: 'duration',
-            header: "Duration",
-            dataIndex: 'duration',
-            renderer: renderDuration
-        }, {
-            width: 250,
-            id: 'channel',
-            header: "Channel",
-            dataIndex: 'channel',
-            renderer: renderText
-        }, {
-            width: 50,
-            id: 'starrating',
-            header: "Stars",
-            dataIndex: 'starrating',
-            renderer: renderInt
-        }, {
-            width: 50,
-            id: 'agerating',
-            header: "Age",
-            dataIndex: 'agerating',
-            renderer: renderInt
-        }, {
-            width: 250,
-            id: 'content_type',
-            header: "Content Type",
-            dataIndex: 'content_type',
-            renderer: function(v) {
-                return tvheadend.contentGroupLookupName(v);
+    var epgCm = new Ext.grid.ColumnModel({
+        defaultSortable: true,
+        columns: [
+            actions,
+            new Ext.ux.grid.ProgressColumn({
+                width: 100,
+                header: "Progress",
+                dataIndex: 'progress',
+                colored: false,
+                ceiling: 100,
+                tvh_renderer: function(value, meta, record, rowIndex, colIndex, store) {
+                    var entry = record.data;
+                    var start = entry.start;           // milliseconds
+                    var duration = entry.stop - start; // milliseconds
+                    var now = new Date();
+
+                    if (!duration || duration < 0) duration = 0;
+                    // Only render a progress bar for currently running programmes
+                    if (now >= start && now - start <= duration)
+                        return (now - start) / duration * 100;
+                    else
+                        return "";
+                }
+            }),
+            {
+                width: 250,
+                id: 'title',
+                header: "Title",
+                dataIndex: 'title',
+                renderer: renderText
+            },
+            {
+                width: 250,
+                id: 'subtitle',
+                header: "SubTitle",
+                dataIndex: 'subtitle',
+                renderer: renderText
+            },
+            {
+                width: 100,
+                id: 'episodeOnscreen',
+                header: "Episode",
+                dataIndex: 'episodeOnscreen',
+                renderer: renderText
+            },
+            {
+                width: 100,
+                id: 'start',
+                header: "Start",
+                dataIndex: 'start',
+                renderer: renderDate
+            },
+            {
+                width: 100,
+                hidden: true,
+                id: 'stop',
+                header: "End",
+                dataIndex: 'stop',
+                renderer: renderDate
+            },
+            {
+                width: 100,
+                id: 'duration',
+                header: "Duration",
+                renderer: renderDuration
+            },
+            {
+                width: 60,
+                id: 'channelNumber',
+                header: "Number",
+                align: 'right',
+                dataIndex: 'channelNumber',
+                renderer: renderText
+            },
+            {
+                width: 250,
+                id: 'channelName',
+                header: "Channel",
+                dataIndex: 'channelName',
+                renderer: renderText
+            },
+            {
+                width: 50,
+                id: 'starRating',
+                header: "Stars",
+                dataIndex: 'starRating',
+                renderer: renderInt
+            },
+            {
+                width: 50,
+                id: 'ageRating',
+                header: "Age",
+                dataIndex: 'ageRating',
+                renderer: renderInt
+            }, {
+                width: 250,
+                id: 'genre',
+                header: "Content Type",
+                dataIndex: 'genre',
+                renderer: function(vals) {
+                    var r = [];
+                    Ext.each(vals, function(v) {
+                        v = tvheadend.contentGroupFullLookupName(v);
+                        if (v)
+                          r.push(v);
+                    });
+                    return r.join(',');
+                }
             }
-        }]);
+        ]
+    });
+
+    var filter = new Ext.ux.grid.GridFilters({
+        encode: true,
+        local: false,
+        filters: [
+            { type: 'string',   dataIndex: 'title' },
+            { type: 'string',   dataIndex: 'subtitle' },
+            { type: 'string',   dataIndex: 'episodeOnscreen' },
+            { type: 'intsplit', dataIndex: 'channelNumber', intsplit: 1000000 },
+            { type: 'string',   dataIndex: 'channelName' },
+            { type: 'numeric',  dataIndex: 'starRating' },
+            { type: 'numeric',  dataIndex: 'ageRating' }
+        ]
+    });
 
     // Title search box
 
@@ -419,7 +473,7 @@ tvheadend.epg = function() {
             blur: function () {
                 if(this.getRawValue() == "" ) {
                     clearChannelFilter();
-                    epgStore.reload();
+                    epgView.reset();
                 }
             }
         }
@@ -441,7 +495,7 @@ tvheadend.epg = function() {
             blur: function () {
                 if(this.getRawValue() == "" ) {
                     clearChannelTagsFilter();
-                    epgStore.reload();
+                    epgView.reset();
                 }
             }
         }
@@ -465,7 +519,7 @@ tvheadend.epg = function() {
             blur: function () {
                 if(this.getRawValue() == "" ) {
                     clearContentGroupFilter();
-                    epgStore.reload();
+                    epgView.reset();
                 }
             }
         }
@@ -486,7 +540,7 @@ tvheadend.epg = function() {
             blur: function () {
                 if(this.getRawValue() == "" ) {
                     clearDurationFilter();
-                    epgStore.reload();
+                    epgView.reset();
                 }
             }
         }
@@ -508,18 +562,18 @@ tvheadend.epg = function() {
     };
 
     clearChannelTagsFilter = function() {
-        delete epgStore.baseParams.tag;
+        delete epgStore.baseParams.channelTag;
         epgFilterChannelTags.setValue("");
     };
 
     clearContentGroupFilter = function() {
-        delete epgStore.baseParams.content_type;
+        delete epgStore.baseParams.contentType;
         epgFilterContentGroup.setValue("");
     };
 
     clearDurationFilter = function() {
-        delete epgStore.baseParams.minduration;
-        delete epgStore.baseParams.maxduration;
+        delete epgStore.baseParams.durationMin;
+        delete epgStore.baseParams.durationMax;
         epgFilterDuration.setValue("");
     };
 
@@ -529,7 +583,9 @@ tvheadend.epg = function() {
         clearChannelTagsFilter();
         clearDurationFilter();
         clearContentGroupFilter();
-        epgStore.reload();
+        filter.clearFilters();
+        delete epgStore.sortInfo;
+        epgView.reset();
     };
 
 /*
@@ -541,33 +597,33 @@ tvheadend.epg = function() {
             clearChannelFilter();
         else if (epgStore.baseParams.channel !== r.data.key)
             epgStore.baseParams.channel = r.data.key;
-        epgStore.reload();
+        epgView.reset();
     });
 
     epgFilterChannelTags.on('select', function(c, r) {
         if (r.data.key == -1)
             clearChannelTagsFilter();
-        else if (epgStore.baseParams.tag !== r.data.key)
-            epgStore.baseParams.tag = r.data.key;
-        epgStore.reload();
+        else if (epgStore.baseParams.channelTag !== r.data.key)
+            epgStore.baseParams.channelTag = r.data.key;
+        epgView.reset();
     });
 
     epgFilterContentGroup.on('select', function(c, r) {
         if (r.data.key == -1)
             clearContentGroupFilter();
-        else if (epgStore.baseParams.content_type !== r.data.key)
-            epgStore.baseParams.content_type = r.data.key;
-        epgStore.reload();
+        else if (epgStore.baseParams.contentType !== r.data.key)
+            epgStore.baseParams.contentType = r.data.key;
+        epgView.reset();
     });
 
     epgFilterDuration.on('select', function(c, r) {
         if (r.data.identifier == -1)
             clearDurationFilter();
-        else if (epgStore.baseParams.minduration !== r.data.minvalue) {
-            epgStore.baseParams.minduration = r.data.minvalue;
-            epgStore.baseParams.maxduration = r.data.maxvalue;
+        else if (epgStore.baseParams.durationMin !== r.data.minvalue) {
+            epgStore.baseParams.durationMin = r.data.minvalue;
+            epgStore.baseParams.durationMax = r.data.maxvalue;
         }
-        epgStore.reload();
+        epgView.reset();
     });
 
     epgFilterTitle.on('valid', function(c) {
@@ -578,7 +634,7 @@ tvheadend.epg = function() {
 
         if (epgStore.baseParams.title !== value) {
             epgStore.baseParams.title = value;
-            epgStore.reload();
+            epgView.reset();
         }
     });
 
@@ -586,6 +642,14 @@ tvheadend.epg = function() {
         nearLimit: 100,
         loadMask: {
             msg: 'Buffering. Please wait...'
+        },
+        listeners: {
+            beforebuffer: {
+                fn: function(view, ds, index, range, total, options) {
+                    /* filters hack */
+                    filter.onBeforeLoad(ds, options);
+                }
+            }
         }
     });
 
@@ -632,7 +696,7 @@ tvheadend.epg = function() {
         stateId: 'epggrid',
         enableDragDrop: false,
         cm: epgCm,
-        plugins: [actions],
+        plugins: [filter, actions],
         title: 'Electronic Program Guide',
         iconCls: 'newspaper',
         store: epgStore,
@@ -646,7 +710,10 @@ tvheadend.epg = function() {
     });
 
     panel.on('rowclick', rowclicked);
-    
+    panel.on('filterupdate', function() {
+        epgView.reset();
+    });
+
     /**
      * Listener for DVR notifications. We want to update the EPG grid when a
      * recording is finished/deleted etc. so the status icon gets updated. 
@@ -660,7 +727,7 @@ tvheadend.epg = function() {
     
     // Always reload the store when the tab is activated
     panel.on('beforeshow', function() {
-        this.store.reload();
+        epgStore.reload();
     });
 
     function rowclicked(grid, index) {
@@ -676,11 +743,11 @@ tvheadend.epg = function() {
                 : "<i>Don't care</i>";
         var channel = epgStore.baseParams.channel ? tvheadend.channelLookupName(epgStore.baseParams.channel)
                 : "<i>Don't care</i>";
-        var tag = epgStore.baseParams.tag ? tvheadend.tagLookupName(epgStore.baseParams.tag)
+        var tag = epgStore.baseParams.channelTag ? tvheadend.channelTagLookupName(epgStore.baseParams.channelTag)
                 : "<i>Don't care</i>";
-        var content_type = epgStore.baseParams.content_type ? tvheadend.contentGroupLookupName(epgStore.baseParams.content_type)
+        var contentType = epgStore.baseParams.contentType ? tvheadend.contentGroupLookupName(epgStore.baseParams.contentType)
                 : "<i>Don't care</i>";
-        var duration = epgStore.baseParams.minduration ? tvheadend.durationLookupRange(epgStore.baseParams.minduration)
+        var duration = epgStore.baseParams.durationMin ? tvheadend.durationLookupRange(epgStore.baseParams.durationMin)
                 : "<i>Don't care</i>";
 
         Ext.MessageBox.confirm('Auto Recorder', 'This will create an automatic rule that '
@@ -689,7 +756,7 @@ tvheadend.epg = function() {
                 + '<div class="x-smallhdr">Title:</div>' + title + '<br>'
                 + '<div class="x-smallhdr">Channel:</div>' + channel + '<br>'
                 + '<div class="x-smallhdr">Tag:</div>' + tag + '<br>'
-                + '<div class="x-smallhdr">Genre:</div>' + content_type + '<br>'
+                + '<div class="x-smallhdr">Genre:</div>' + contentType + '<br>'
                 + '<div class="x-smallhdr">Duration:</div>' + duration + '<br>'
                 + '<br><br>' + 'Currently this will match (and record) '
                 + epgStore.getTotalCount() + ' events. ' + 'Are you sure?',
@@ -709,9 +776,9 @@ tvheadend.epg = function() {
         if (params.title) conf.title = params.title;
         if (params.channel) conf.channel = params.channel;
         if (params.tag) conf.tag = params.tag;
-        if (params.content_type) conf.content_type = params.content_type;
-        if (params.minduration) conf.minduration = params.minduration;
-        if (params.maxduration) conf.maxduration = params.maxduration;
+        if (params.contentType) conf.content_type = params.contentType;
+        if (params.durationMin) conf.minduration = params.durationMin;
+        if (params.durationMax) conf.maxduration = params.durationMax;
         Ext.Ajax.request({
             url: 'api/dvr/autorec/create',
             params: { conf: Ext.encode(conf) }