diff --git a/docs/docresources/autorec.png b/docs/docresources/autorec.png index 68422659..7917c042 100644 Binary files a/docs/docresources/autorec.png and b/docs/docresources/autorec.png differ diff --git a/docs/docresources/autorecpopup.png b/docs/docresources/autorecpopup.png index 6b25f680..31929dc7 100644 Binary files a/docs/docresources/autorecpopup.png and b/docs/docresources/autorecpopup.png differ diff --git a/docs/docresources/dvrlog.png b/docs/docresources/dvrlog.png index 49cbe37f..8b40a178 100644 Binary files a/docs/docresources/dvrlog.png and b/docs/docresources/dvrlog.png differ diff --git a/docs/docresources/dvrlog2.png b/docs/docresources/dvrlog2.png new file mode 100644 index 00000000..ae4c0e57 Binary files /dev/null and b/docs/docresources/dvrlog2.png differ diff --git a/docs/docresources/dvrlog3.png b/docs/docresources/dvrlog3.png new file mode 100644 index 00000000..76080d35 Binary files /dev/null and b/docs/docresources/dvrlog3.png differ diff --git a/docs/docresources/dvrlog4.png b/docs/docresources/dvrlog4.png new file mode 100644 index 00000000..f4ae5b6b Binary files /dev/null and b/docs/docresources/dvrlog4.png differ diff --git a/docs/docresources/epg.png b/docs/docresources/epg.png index e359399f..bae7990b 100644 Binary files a/docs/docresources/epg.png and b/docs/docresources/epg.png differ diff --git a/docs/docresources/epg2.png b/docs/docresources/epg2.png index e9986b98..3ea05bee 100644 Binary files a/docs/docresources/epg2.png and b/docs/docresources/epg2.png differ diff --git a/docs/docresources/epg3.png b/docs/docresources/epg3.png new file mode 100644 index 00000000..78d33421 Binary files /dev/null and b/docs/docresources/epg3.png differ diff --git a/docs/html/autorec.html b/docs/html/autorec.html index 89115c32..c6b53846 100644 --- a/docs/html/autorec.html +++ b/docs/html/autorec.html @@ -1,5 +1,5 @@
-The 'Automatic Recorder' is used to create rules that will trig +The 'Automatic Recorder' is used to create rules that will trigger automatic recording of events. You can use this to record you favorite TV show(s), record all movies on a specific channel, etc.

@@ -62,6 +62,10 @@ The columns have the following functions:

Only match events belonging to the given content group. +
Duration +
+ Only match events that fall within the specified duration range. +
Weekdays
Only record events if they occur on one of these days. By default all days diff --git a/docs/html/dvrlog.html b/docs/html/dvrlog.html index d29b53aa..612b86dc 100644 --- a/docs/html/dvrlog.html +++ b/docs/html/dvrlog.html @@ -1,27 +1,45 @@
The DVR log is split into a series of paged grids: +

+
    -
  • Upcoming Recordings - stuff scheduled to be recorded in the future -
  • Finished Recordings - stuff that has succesfully finished recording -
  • Failed Recordings - stuff that failed to record -
+
  • Upcoming Recordings - events that are scheduled to be recorded in the future
  • + + + +
  • Finished Recordings - events that have succesfully finished recording
  • + +

    -Use the bottom toolbar (not displayed in this manual) to navigate -between pages in the grid. - +Once the recording is completed there will be a clickable link to a playlist +for the recorded file (XSPF or M3U as per your startup options) so you can watch +it.

    + + +
  • Failed Recordings - events that failed to record
  • + + + + + +

    +Use the bottom toolbar to navigate between pages in the grid.

    + + + +

    Note that the columns are sortable, but only the current view will be sorted (by +default, this will be the first page of the most recent events). Select more events +per page if you want to sort a longer selection.

    To see more details about a recorded event, just click on it and a pop -up will appear: -

    +up will appear:

    +

    +

    In this pop up you can cancel a scheduled recording or abort a recording in progress. To close the pop up, just close it with the -[X] window button. -

    -Once the recording is completed there will be a clickable link to the -recorded matroska file so you can download it directly from the -interface. +[X] window button.

    diff --git a/docs/html/epg.html b/docs/html/epg.html index 13118c8f..615d3202 100644 --- a/docs/html/epg.html +++ b/docs/html/epg.html @@ -1,95 +1,130 @@
    -Tvheadend has a built in Electronic Program Guide. The EPG is an in memory +

    Tvheadend has a built-in Electronic Program Guide. The EPG is an in-memory database populated with all the information about events received from -the DVB networks or from XMLTV. -

    -The EPG tab displays a filterable paged grid containing all the events -sorted based on start time. +the DVB networks over-the-air or from external grabbers such as XMLTV.

    +

    +

    The EPG tab displays a filterable grid containing all events, +sorted based on start time.

    -

    -

    -
    Filtering (or searching) -
    - In the EPG top tool bar you can access four input fields. +

    +
    +Filtering (or searching) +
    +

    In the EPG top tool bar you can access five input fields. These are used to filter/search for events. The form uses implicit AND between the input fields. This means that all filters must match - for an event to be displayed. + for an event to be displayed.

    [Search title...]
    - Filter on the event title. The filter uses case insensitive regular - expression. If you don't know what a regular expression is this means - that you can type just parts of the title and filter on that too. - (No need for exact matching). -
    [Filter channel...] + Only display events that match the given title. The filter uses case-insensitive + regular expressions. If you don't know what a regular expression is, this simply + means that you can type just parts of the title and filter on that - there's no need + for full, exact matching.
    +
    [Filter channel...]
    Only display events from the selected channel. Channels in the drop down are - ordered by channel number and can be filtered (by name) by typing in the box. -
    [Filter tag...] + ordered by channel number and can be filtered (by name) by typing in the box.
    +
    [Filter tag...]
    - Only display events from the channels which are included in the selected tag. - Tags are used for grouping channels and is configured by the administrator. -
    [Filter content type...] + Only display events from channels which are included in the selected tag. + Tags are used for grouping channels together - such as 'Radio' or 'HDTV' - and are + configured by the administrator. You can start typing a tag name to filter the list. +
    [Filter content type...]
    - Most DVB networks classify their events into content groups. This field - allows you to filter based on content type. + Only display events that match the given content type tag. Most DVB networks + classify their events into content groups. This field allows you to filter based + on content type (e.g. "Sports" or "Game Show"). Supported tags are determined by + your broadcaster. Again, simply start typing to filter the entries if you have a + long list to choose from.
    +
    [Filter duration...]
    +
    + Only display events that fall between the given minimum and maximum durations. + This allows you to filter for or against, say, a daily broadcast and a weekly omnibus + edition of a programme, or only look for short news bulletins and not the 24-hour + rolling broadcasts.
    +
     
    +
    Options are:
    + + + + + + + +
    00:00:01 to 00:15:00 - for very short news bulletins, children's programmes, etc.
    00:15:01 to 00:30:00 - for short programmes, e.g. daily soap operas
    00:30:01 to 01:30:00 - for medium-length programmes, e.g. documentaries
    01:30:01 to 03:00:00 - for longer programmes, e.g. films
    03:00:00 to no maximum - for very long programmes, e.g. major sporting events
    + - Thus, if you only would like to browse Movies from your HD-channels you - would select 'HDTV' in the [Filter tag...]-field, and select - 'Movie / Drama' in the [Only include content...]-field. -

    - Notice that you don't have to press a 'Search' button, the grid immediately - updates itself as you change the filters. -

    - If you would like to clear all filters, just press the [Reset] button. -

    Paging +

    So, if you only want to see Movies from your available HD channels, you + would select 'HDTV' in the [Filter tag...] field, and select + 'Movie / Drama' in the [Filter content type...] field. If you wish, you + could then further limit the search to programmes of between 90 minutes and 3 hours by + selecting '01:30:01 to 03:00:00' in the [Filter duration...] field.

    +

    +

    Note that you don't have to press a 'Search' button: the grid immediately + updates itself as you change the filters.

    +

    +

    You can clear an individual filter by simply deleting its contents, or by selecting + '(Clear filter)' as appropriate on all except the title filter. If you want to + clear all filters, just press the [Reset All] button.

    +
     
    -
    - In large installations with many channels and full EPG feed there could be - tens of thousands of events in the database. - Therefore the EPG display employs a paging bar at the bottom of the grid. - Use it to browse backwards and forwards in the EPG. It also displays the - total amount of events matched by the current query. +
    +Event details and recording +
    -
    Event details and recording -
    - If you click on a single event, a popup will display detailed information - about the event. It also allows the user to schedule the event for recording - by clicking on the [Record program] button. +

    If you click on a single event, a popup will display detailed information + about the event. It also allows you to schedule the event for recording + by clicking on the [Record program] button.

    For EPG providers that supply series link information there will also be a - [Record series] button that will record all entries in the series. + [Record series] button that will record all entries in the series.

    - For events without any series link information, a [Autorec] button will be - provided to create a pseudo series link using the Autorec feature. -

    - -

    - To close the popup, just close it with the [X] window button. - The popup is not modal and you can open as many detailed information popups - as you want. + For events without any series link information, an [Autorec] button will be + provided to create a pseudo-series link using the autorec feature.

    +

    + +

    +

    If you schedule any kind of recording from this point, you can choose a specific + DVR profile that will apply to the recording or autorec rule. This will normally show + as (default), but you can define different profiles in the Configuration -> + Recording -> Digital Video Recorder tab. This allows you to set, for example, more post- + broadcast padding for a channel that always runs late, or perhaps define a different + post-processing command to strip adverts out on a commercial channel.

    +

    You will also see a Search IMDB link to look for the programme by name on + imdb.com, and a Play link to watch a programme that's already in progress. This + second link downloads a playlist file (XSPF or M3U depending on your startup options); + if your system is configured for it, this will automatically launch an appropriate + player, otherwise you will need to manually open the playlist to start watching (normally a + double-click on the downloaded file).

    +

    +

    To close the popup, just click on the [X] window button. + The popup isn't modal, so you don't have to close it before doing something else, + and you can open as many detailed information popups as you want.

    +
     
    - -
    Autorecordings -
    - Should you wish to record all events matching a specific query. (Record - your favorite TV-show, etc) you can press the [Create Autorec] button - in the top toolbar. -

    - A popup with details about the to-be-created autorecording rule needs to - be confirmed before the rule takes effect. -

    +


    + Autorecordings +
    +

    Should you wish to record all events matching a specific query (to record + your favourite show every week, for example) you can press the [Create AutoRec] + button in the top toolbar.

    +

    +

    A popup with details about the to-be-created autorecording rule needs to + be confirmed before the rule takes effect.

    +

    -

    - The autorecordings can later be changed/deleted in under the - 'Digital Video Recorder'-tag. Use that editor if you temporary want - to disable an autorecording or make adjustments, etc. +

    +

    You can change or delete the autorec rules in the + Digital Video Recorder tab. Use that editor if you temporarily want + to disable an autorecording or make adjustments to the channel, tag, or similar.

    +
     
    -
    Watch TV -
    - If you want to watch live TV in the web UI, the [Watch TV] button will pop-up +
    + Watch TV +
    +

    If you want to watch live TV in the web UI, the [Watch TV] button will pop up a VLC plugin window (if you don't have the plugin installed a direct URL should be - provided to load into your preferred media player). - + provided to load into your preferred media player).

    diff --git a/src/api/api_epg.c b/src/api/api_epg.c index 5db682b3..9bd44903 100644 --- a/src/api/api_epg.c +++ b/src/api/api_epg.c @@ -130,6 +130,8 @@ api_epg_grid const char *ch, *tag, *title, *lang/*, *genre*/; uint32_t start, limit, end; htsmsg_t *l = NULL, *e; + int min_duration; + int max_duration; *resp = htsmsg_create_map(); @@ -141,13 +143,16 @@ api_epg_grid lang = htsmsg_get_str(args, "lang"); // TODO: support multiple tag/genre/channel? + min_duration = htsmsg_get_u32_or_default(args, "minduration", 0); + max_duration = htsmsg_get_u32_or_default(args, "maxduration", INT_MAX); + /* Pagination settings */ start = htsmsg_get_u32_or_default(args, "start", 0); limit = htsmsg_get_u32_or_default(args, "limit", 50); /* Query the EPG */ pthread_mutex_lock(&global_lock); - epg_query(&eqr, ch, tag, NULL, /*genre,*/ title, lang); + epg_query(&eqr, ch, tag, NULL, /*genre,*/ title, lang, min_duration, max_duration); epg_query_sort(&eqr); // TODO: optional sorting diff --git a/src/dvr/dvr.h b/src/dvr/dvr.h index 71bc1631..4cf13149 100644 --- a/src/dvr/dvr.h +++ b/src/dvr/dvr.h @@ -247,6 +247,8 @@ typedef struct dvr_autorec_entry { epg_serieslink_t *dae_serieslink; epg_episode_num_t dae_epnum; + int dae_minduration; + int dae_maxduration; } dvr_autorec_entry_t; @@ -386,12 +388,13 @@ int dvr_sort_start_ascending(const void *A, const void *B); */ void dvr_autorec_add(const char *dvr_config_name, const char *title, const char *channel, - const char *tag, epg_genre_t *content_type, - const char *creator, const char *comment); + const char *tag, epg_genre_t *content_type, + const int min_duration, const int max_duration, + const char *creator, const char *comment); void dvr_autorec_add_series_link(const char *dvr_config_name, epg_broadcast_t *event, - const char *creator, const char *comment); + const char *creator, const char *comment); void dvr_autorec_check_event(epg_broadcast_t *e); void dvr_autorec_check_brand(epg_brand_t *b); diff --git a/src/dvr/dvr_autorec.c b/src/dvr/dvr_autorec.c index ec92ce3d..c21c7bda 100644 --- a/src/dvr/dvr_autorec.c +++ b/src/dvr/dvr_autorec.c @@ -26,6 +26,7 @@ #include #include #include +#include #include "tvheadend.h" #include "settings.h" @@ -70,6 +71,7 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e) { channel_tag_mapping_t *ctm; dvr_config_t *cfg; + double duration; if (!e->channel) return 0; if (!e->episode) return 0; @@ -83,6 +85,8 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e) dae->dae_title[0] == '\0') && dae->dae_brand == NULL && dae->dae_season == NULL && + &dae->dae_minduration == NULL && + &dae->dae_maxduration == NULL && dae->dae_serieslink == NULL) return 0; // Avoid super wildcard match @@ -136,6 +140,16 @@ autorec_cmp(dvr_autorec_entry_t *dae, epg_broadcast_t *e) return 0; } + duration = difftime(e->stop,e->start); + + if(dae->dae_minduration) { + if(duration < dae->dae_minduration) return 0; + } + + if(dae->dae_maxduration) { + if(duration > dae->dae_maxduration) return 0; + } + if(dae->dae_weekdays != 0x7f) { struct tm tm; localtime_r(&e->start, &tm); @@ -283,6 +297,11 @@ autorec_record_build(dvr_autorec_entry_t *dae) build_weekday_tags(l, dae->dae_weekdays); htsmsg_add_msg(e, "weekdays", l); + if (dae->dae_minduration) + htsmsg_add_u32(e, "minduration", dae->dae_minduration); + if (dae->dae_maxduration) + htsmsg_add_u32(e, "maxduration", dae->dae_maxduration); + htsmsg_add_str(e, "pri", dvr_val2pri(dae->dae_pri)); if (dae->dae_brand) @@ -407,6 +426,12 @@ autorec_record_update(void *opaque, const char *id, htsmsg_t *values, } } + if(!htsmsg_get_u32(values, "minduration", &u32)) + dae->dae_minduration = u32; + + if(!htsmsg_get_u32(values, "maxduration", &u32)) + dae->dae_maxduration = u32; + if((l = htsmsg_get_list(values, "weekdays")) != NULL) dae->dae_weekdays = build_weekday_mask(l); @@ -506,7 +531,8 @@ dvr_autorec_update(void) static void _dvr_autorec_add(const char *config_name, const char *title, channel_t *ch, - const char *tag, epg_genre_t *content_type, + const char *tag, epg_genre_t *content_type, + const int min_duration, const int max_duration, epg_brand_t *brand, epg_season_t *season, epg_serieslink_t *serieslink, int approx_time, epg_episode_num_t *epnum, @@ -543,6 +569,12 @@ _dvr_autorec_add(const char *config_name, if (content_type) dae->dae_content_type.code = content_type->code; + if (min_duration) + dae->dae_minduration = min_duration; + + if (max_duration) + dae->dae_maxduration = max_duration; + if(serieslink) { serieslink->getref(serieslink); dae->dae_serieslink = serieslink; @@ -564,12 +596,14 @@ _dvr_autorec_add(const char *config_name, void dvr_autorec_add(const char *config_name, const char *title, const char *channel, - const char *tag, epg_genre_t *content_type, - const char *creator, const char *comment) + const char *tag, epg_genre_t *content_type, + const int min_duration, const int max_duration, + const char *creator, const char *comment) { channel_t *ch = NULL; if(channel != NULL) ch = channel_find(channel); _dvr_autorec_add(config_name, title, ch, tag, content_type, + min_duration, max_duration, NULL, NULL, NULL, 0, NULL, creator, comment); } @@ -584,6 +618,7 @@ void dvr_autorec_add_series_link title, event->channel, NULL, 0, // tag/content type + 0,INT_MAX, NULL, NULL, event->serieslink, diff --git a/src/epg.c b/src/epg.c index 03322ccd..3ed3da16 100644 --- a/src/epg.c +++ b/src/epg.c @@ -23,6 +23,7 @@ #include #include #include +#include #include "tvheadend.h" #include "queue.h" @@ -2205,9 +2206,10 @@ 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, const char *lang ) + 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; @@ -2215,6 +2217,9 @@ static void _eqr_add 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; + /* More space */ if ( eqr->eqr_entries == eqr->eqr_alloced ) { eqr->eqr_alloced = MAX(100, eqr->eqr_alloced * 2); @@ -2228,17 +2233,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, const char *lang ) + regex_t *preg, time_t start, const char *lang, int min_duration, int max_duration ) { epg_broadcast_t *ebc; RB_FOREACH(ebc, &ch->ch_epg_schedule, sched_link) { - if ( ebc->episode ) _eqr_add(eqr, ebc, genre, preg, start, lang); + if ( ebc->episode ) _eqr_add(eqr, ebc, genre, preg, start, lang, min_duration, max_duration); } } 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 ) + epg_genre_t *genre, const char *title, const char *lang, int min_duration, int max_duration ) { time_t now; channel_tag_mapping_t *ctm; @@ -2259,19 +2264,19 @@ void epg_query0 /* Single channel */ if (channel && !tag) { - _eqr_add_channel(eqr, channel, genre, preg, now, lang); + _eqr_add_channel(eqr, channel, genre, preg, now, lang, min_duration, max_duration); /* 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, lang); + _eqr_add_channel(eqr, ctm->ctm_channel, genre, preg, now, lang, min_duration, max_duration); } /* All channels */ } else { CHANNEL_FOREACH(channel) - _eqr_add_channel(eqr, channel, genre, preg, now, lang); + _eqr_add_channel(eqr, channel, genre, preg, now, lang, min_duration, max_duration); } if (preg) regfree(preg); @@ -2279,11 +2284,12 @@ void epg_query0 } void epg_query(epg_query_result_t *eqr, const char *channel, const char *tag, - epg_genre_t *genre, const char *title, const char *lang) + 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_name(tag, 0) : NULL; - epg_query0(eqr, ch, ct, genre, title, lang); + + epg_query0(eqr, ch, ct, genre, title, lang, min_duration, max_duration); } void epg_query_free(epg_query_result_t *eqr) diff --git a/src/epg.h b/src/epg.h index 0f177492..1591f9cd 100644 --- a/src/epg.h +++ b/src/epg.h @@ -546,9 +546,9 @@ 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); + 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); + epg_genre_t *genre, const char *title, const char *lang, int min_duration, int max_duration); /* ************************************************************************ diff --git a/src/htsp_server.c b/src/htsp_server.c index f9568541..ea2a625c 100644 --- a/src/htsp_server.c +++ b/src/htsp_server.c @@ -57,6 +57,7 @@ #include #include "settings.h" #include +#include /* ************************************************************************** * Datatypes and variables @@ -1059,7 +1060,9 @@ htsp_method_epgQuery(htsp_connection_t *htsp, htsmsg_t *in) epg_query_result_t eqr; epg_genre_t genre, *eg = NULL; const char *lang; - + int min_duration; + int max_duration; + /* Required */ if( (query = htsmsg_get_str(in, "query")) == NULL ) return htsp_error("Missing argument 'query'"); @@ -1079,12 +1082,16 @@ htsp_method_epgQuery(htsp_connection_t *htsp, htsmsg_t *in) lang = htsmsg_get_str(in, "language") ?: htsp->htsp_language; 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); + 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); + epg_query0(&eqr, ch, ct, eg, query, lang, min_duration, max_duration); // create reply out = htsmsg_create_map(); diff --git a/src/webui/extjs.c b/src/webui/extjs.c index b76a43ef..9b27431f 100755 --- a/src/webui/extjs.c +++ b/src/webui/extjs.c @@ -759,9 +759,22 @@ extjs_epg(http_connection_t *hc, const char *remain, void *opaque) 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); @@ -780,7 +793,7 @@ extjs_epg(http_connection_t *hc, const char *remain, void *opaque) pthread_mutex_lock(&global_lock); - epg_query(&eqr, channel, tag, eg, title, lang); + epg_query(&eqr, channel, tag, eg, title, lang, min_duration, max_duration); epg_query_sort(&eqr); @@ -1117,18 +1130,31 @@ extjs_dvr(http_connection_t *hc, const char *remain, void *opaque) htsmsg_add_u32(out, "success", 1); } else if(!strcmp(op, "createAutoRec")) { + int min_duration; + int max_duration; epg_genre_t genre, *eg = NULL; + if ((s = http_arg_get(&hc->hc_req_args, "contenttype"))) { genre.code = atoi(s); eg = &genre; } + 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; + dvr_autorec_add(http_arg_get(&hc->hc_req_args, "config_name"), http_arg_get(&hc->hc_req_args, "title"), - http_arg_get(&hc->hc_req_args, "channel"), - http_arg_get(&hc->hc_req_args, "tag"), - eg, - hc->hc_representative, "Created from EPG query"); + http_arg_get(&hc->hc_req_args, "channel"), + http_arg_get(&hc->hc_req_args, "tag"), + eg, min_duration,max_duration, + hc->hc_representative, "Created from EPG query"); out = htsmsg_create_map(); htsmsg_add_u32(out, "success", 1); diff --git a/src/webui/simpleui.c b/src/webui/simpleui.c index cfb12c5b..4bea56e2 100644 --- a/src/webui/simpleui.c +++ b/src/webui/simpleui.c @@ -87,8 +87,9 @@ page_simple(http_connection_t *hc, if(s != NULL) { - - epg_query(&eqr, NULL, NULL, NULL, s, 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); c = eqr.eqr_entries; diff --git a/src/webui/static/app/chconf.js b/src/webui/static/app/chconf.js index 1b70bfb1..421c78d3 100644 --- a/src/webui/static/app/chconf.js +++ b/src/webui/static/app/chconf.js @@ -1,6 +1,11 @@ /** * Channel tags */ +insertChannelTagsClearOption = function( scope, records, options ){ + var placeholder = Ext.data.Record.create(['identifier', 'name']); + scope.insert(0,new placeholder({identifier: '-1', name: '(Clear filter)'})); +}; + tvheadend.channelTags = new Ext.data.JsonStore({ autoLoad: true, root: 'entries', @@ -9,6 +14,9 @@ tvheadend.channelTags = new Ext.data.JsonStore({ url: 'channeltags', baseParams: { op: 'listTags' + }, + listeners: { + 'load': insertChannelTagsClearOption } }); @@ -26,6 +34,11 @@ tvheadend.channelrec = new Ext.data.Record.create( ['name', 'chid', 'epggrabsrc', 'tags', 'ch_icon', 'epg_pre_start', 'epg_post_end', 'number']); +insertChannelClearOption = function( scope, records, options ){ + var placeholder = Ext.data.Record.create(['key', 'val']); + scope.insert(0,new placeholder({key: '-1', val: '(Clear filter)'})); +}; + tvheadend.channels = new Ext.data.JsonStore({ url: 'api/channel/list', root: 'entries', @@ -35,6 +48,9 @@ tvheadend.channels = new Ext.data.JsonStore({ sortInfo: { field: 'val', direction: 'ASC' + }, + listeners: { + 'load': insertChannelClearOption } }); diff --git a/src/webui/static/app/dvr.js b/src/webui/static/app/dvr.js index 6705884e..06ab8a3f 100644 --- a/src/webui/static/app/dvr.js +++ b/src/webui/static/app/dvr.js @@ -507,10 +507,6 @@ tvheadend.dvrschedule = function(title, iconCls, dvrStore) { return panel; }; -/** - * - */ - /** * */ @@ -543,16 +539,15 @@ tvheadend.autoreceditor = function() { valueField: 'key', store: tvheadend.channels, mode: 'local', - editable: false, + editable: true, + forceSelection: true, + typeAhead: true, triggerAction: 'all', emptyText: 'Only include channel...' }), - renderer: function(v, m, r) { - var i = tvheadend.channels.find('key', v); - if (i !== -1) - v = tvheadend.channels.getAt(i).get('val'); - return v; - } + renderer: function(v) { + return tvheadend.channelLookupName(v); + }, }, { header: "SeriesLink", @@ -568,7 +563,9 @@ tvheadend.autoreceditor = function() { displayField: 'name', store: tvheadend.channelTags, mode: 'local', - editable: false, + editable: true, + forceSelection: true, + typeAhead: true, triggerAction: 'all', emptyText: 'Only include tag...' }) @@ -584,11 +581,31 @@ tvheadend.autoreceditor = function() { displayField: 'name', store: tvheadend.ContentGroupStore, mode: 'local', - editable: false, + editable: true, + forceSelection: true, + typeAhead: true, triggerAction: 'all', emptyText: 'Only include content...' }) }, + { + header: "Duration", + dataIndex: 'minduration', + renderer: function(v) { + return tvheadend.durationLookupRange(v); + }, + editor: durationCombo = new Ext.form.ComboBox({ + store: tvheadend.DurationStore, + mode: 'local', + valueField: 'minvalue', + displayField: 'label', + editable: true, + forceSelection: true, + typeAhead: true, + triggerAction: 'all', + id: 'minfield' + }) + }, { header: "Weekdays", dataIndex: 'weekdays', @@ -685,10 +702,35 @@ tvheadend.autoreceditor = function() { }) }]}); + tvheadend.autorecStore.on('update', function (store, record, operation) { + if (operation == 'edit') { + if (record.isModified('minduration')) { + if (record.data.minduration == "") + record.set('maxduration',""); + else { + var index = tvheadend.DurationStore.find('minvalue', record.data.minduration); + + if (index !== -1) + record.set('maxduration', tvheadend.DurationStore.getById(index).data.maxvalue); + } + } + + if (record.isModified('channel') && record.data.channel == -1) + record.set('channel',""); + + if (record.isModified('tag') && record.data.tag == '(Clear filter)') + record.set('tag',""); + + if (record.isModified('contenttype') && record.data.contenttype == -1) + record.set('contenttype',""); + } + }); + return new tvheadend.tableEditor('Automatic Recorder', 'autorec', cm, tvheadend.autorecRecord, [], tvheadend.autorecStore, 'autorec.html', 'wand'); }; + /** * */ @@ -786,6 +828,7 @@ tvheadend.dvr = function() { tvheadend.autorecRecord = Ext.data.Record.create(['enabled', 'title', 'serieslink', 'channel', 'tag', 'creator', 'contenttype', 'comment', + 'minduration', 'maxduration', 'weekdays', 'pri', 'approx_time', 'config_name']); tvheadend.autorecStore = new Ext.data.JsonStore({ diff --git a/src/webui/static/app/epg.js b/src/webui/static/app/epg.js index a2775fe3..588b2ab2 100644 --- a/src/webui/static/app/epg.js +++ b/src/webui/static/app/epg.js @@ -7,13 +7,22 @@ tvheadend.brands = new Ext.data.JsonStore({ op: 'brandList' } }); + +insertContentGroupClearOption = function( scope, records, options ){ + var placeholder = Ext.data.Record.create(['name', 'code']); + scope.insert(0,new placeholder({name: '(Clear filter)', code: '-1'})); +}; + //WIBNI: might want this store to periodically update tvheadend.ContentGroupStore = new Ext.data.JsonStore({ root: 'entries', fields: ['name', 'code'], autoLoad: true, - url: 'ecglist' + url: 'ecglist', + listeners: { + 'load': insertContentGroupClearOption + } }); tvheadend.contentGroupLookupName = function(code) { @@ -29,6 +38,45 @@ tvheadend.contentGroupLookupName = function(code) { tvheadend.ContentGroupStore.setDefaultSort('code', 'ASC'); +tvheadend.channelLookupName = function(key) { + channelString = ""; + + var index = tvheadend.channels.find('key', key); + + if (index !== -1) + var channelString = tvheadend.channels.getAt(index).get('val'); + + return channelString; +}; + +// Store for duration filters - EPG, autorec dialog and autorec rules in the DVR grid +// NB: 'no max' is defined as 9999999s, or about 3 months... + +tvheadend.DurationStore = new Ext.data.SimpleStore({ + storeId: 'durationnames', + idIndex: 0, + fields: ['identifier','label','minvalue','maxvalue'], + data: [['-1', '(Clear filter)',"",""], + ['1','00:00:01 - 00:15:00',1, 900], + ['2','00:15:01 - 00:30:00', 901, 1800], + ['3','00:30:01 - 01:30:00', 1801, 5400], + ['4','01:30:01 - 03:00:00', 5401, 10800], + ['5','03:00:01 - No maximum', 10801, 9999999]] +}); + +// Function to convert numeric duration to corresponding label string +// Note: triggered by minimum duration only. This would fail if ranges +// had the same minimum (e.g. 15-30 mins and 15-60 minutes) (which we don't have). + +tvheadend.durationLookupRange = function(value) { + durationString = ""; + var index = tvheadend.DurationStore.find('minvalue', value); + if (index !== -1) + var durationString = tvheadend.DurationStore.getAt(index).data.label; + + return durationString; +}; + tvheadend.epgDetails = function(event) { var content = ''; @@ -378,7 +426,16 @@ tvheadend.epg = function() { editable: true, forceSelection: true, triggerAction: 'all', - emptyText: 'Filter channel...' + typeAhead: true, + emptyText: 'Filter channel...', + listeners: { + blur: function () { + if(this.getRawValue() == "" ) { + clearChannelFilter(); + epgStore.reload(); + } + } + } }); // Tags, uses global store @@ -391,7 +448,17 @@ tvheadend.epg = function() { editable: true, forceSelection: true, triggerAction: 'all', - emptyText: 'Filter tag...' + typeAhead: true, + emptyText: 'Filter tag...', + listeners: { + blur: function () { + if(this.getRawValue() == "" ) { + clearChannelTagsFilter(); + epgStore.reload(); + } + } + } + }); // Content groups @@ -405,42 +472,115 @@ tvheadend.epg = function() { editable: true, forceSelection: true, triggerAction: 'all', - emptyText: 'Filter content type...' + typeAhead: true, + emptyText: 'Filter content type...', + listeners: { + blur: function () { + if(this.getRawValue() == "" ) { + clearContentGroupFilter(); + epgStore.reload(); + } + } + } }); - function epgQueryClear() { - delete epgStore.baseParams.channel; - delete epgStore.baseParams.tag; - delete epgStore.baseParams.contenttype; + var epgFilterDuration = new Ext.form.ComboBox({ + loadingText: 'Loading...', + width: 150, + displayField: 'label', + store: tvheadend.DurationStore, + mode: 'local', + editable: true, + forceSelection: true, + triggerAction: 'all', + typeAhead: true, + emptyText: 'Filter duration...', + listeners: { + blur: function () { + if(this.getRawValue() == "" ) { + clearDurationFilter(); + epgStore.reload(); + } + } + } + + }); + +/* + * Clear filter functions + */ + + clearTitleFilter = function() { delete epgStore.baseParams.title; - - epgFilterChannels.setValue(""); - epgFilterChannelTags.setValue(""); - epgFilterContentGroup.setValue(""); epgFilterTitle.setValue(""); + }; + clearChannelFilter = function() { + delete epgStore.baseParams.channel; + epgFilterChannels.setValue(""); + }; + + clearChannelTagsFilter = function() { + delete epgStore.baseParams.tag; + epgFilterChannelTags.setValue(""); + }; + + clearContentGroupFilter = function() { + delete epgStore.baseParams.contenttype; + epgFilterContentGroup.setValue(""); + }; + + clearDurationFilter = function() { + delete epgStore.baseParams.minduration; + delete epgStore.baseParams.maxduration; + epgFilterDuration.setValue(""); + }; + + function epgQueryClear() { + clearTitleFilter(); + clearChannelFilter(); + clearChannelTagsFilter(); + clearDurationFilter(); + clearContentGroupFilter(); epgStore.reload(); - } + }; + +/* + * Filter selection event handlers + */ epgFilterChannels.on('select', function(c, r) { - if (epgStore.baseParams.channel !== r.data.key) { + if (r.data.key == -1) + clearChannelFilter(); + else if (epgStore.baseParams.channel !== r.data.key) epgStore.baseParams.channel = r.data.key; - epgStore.reload(); - } + epgStore.reload(); }); epgFilterChannelTags.on('select', function(c, r) { - if (epgStore.baseParams.tag !== r.data.name) { + if (r.data.identifier == -1) + clearChannelTagsFilter(); + else if (epgStore.baseParams.tag !== r.data.name) epgStore.baseParams.tag = r.data.name; - epgStore.reload(); - } + epgStore.reload(); }); epgFilterContentGroup.on('select', function(c, r) { - if (epgStore.baseParams.contenttype !== r.data.code) { + if (r.data.code == -1) + clearContentGroupFilter(); + else if (epgStore.baseParams.contenttype !== r.data.code) epgStore.baseParams.contenttype = r.data.code; - epgStore.reload(); + epgStore.reload(); + }); + + 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; } + epgStore.reload(); }); epgFilterTitle.on('valid', function(c) { @@ -482,8 +622,10 @@ tvheadend.epg = function() { '-', epgFilterContentGroup, '-', + epgFilterDuration, + '-', { - text: 'Reset', + text: 'Reset All', handler: epgQueryClear }, '->', @@ -524,28 +666,31 @@ tvheadend.epg = function() { var title = epgStore.baseParams.title ? epgStore.baseParams.title : "Don't care"; - var channel = epgStore.baseParams.channel ? epgStore.baseParams.channel + var channel = epgStore.baseParams.channel ? tvheadend.channelLookupName(epgStore.baseParams.channel) : "Don't care"; var tag = epgStore.baseParams.tag ? epgStore.baseParams.tag : "Don't care"; - var contenttype = epgStore.baseParams.contenttype ? epgStore.baseParams.contenttype + var contenttype = epgStore.baseParams.contenttype ? tvheadend.contentGroupLookupName(epgStore.baseParams.contenttype) + : "Don't care"; + var duration = epgStore.baseParams.minduration ? tvheadend.durationLookupRange(epgStore.baseParams.minduration) : "Don't care"; - Ext.MessageBox.confirm('Auto Recorder', - 'This will create an automatic rule that ' + + Ext.MessageBox.confirm('Auto Recorder', 'This will create an automatic rule that ' + 'continuously scans the EPG for programmes ' - + 'to record that matches this query: ' + '

    ' + + 'to record that match this query: ' + '

    ' + '
    Title:
    ' + title + '
    ' + '
    Channel:
    ' + channel + '
    ' + '
    Tag:
    ' + tag + '
    ' + '
    Genre:
    ' + contenttype + '
    ' - + '
    ' + 'Currently this will match (and record) ' + + '
    Duration:
    ' + duration + '
    ' + + '

    ' + 'Currently this will match (and record) ' + epgStore.getTotalCount() + ' events. ' + 'Are you sure?', - function(button) { - if (button === 'no') - return; - createAutoRec2(epgStore.baseParams); - }); + function(button) { + if (button === 'no') + return; + createAutoRec2(epgStore.baseParams); + }); } function createAutoRec2(params) {