diff options
| author | Dave <vdr@pickles.me.uk> | 2011-08-21 09:22:34 +0100 |
|---|---|---|
| committer | Dave <vdr@pickles.me.uk> | 2011-08-21 09:22:34 +0100 |
| commit | bf74a992bb85b026802eae35f884c949706c3d1f (patch) | |
| tree | 613875c88ca8eaac6187b6b98d98d56dbdf55cb7 | |
| parent | 7a02673d36a45482a111687df03c652560a07468 (diff) | |
| download | vdrtva-bf74a992bb85b026802eae35f884c949706c3d1f.tar.gz vdrtva-bf74a992bb85b026802eae35f884c949706c3d1f.tar.bz2 | |
Initial plugin release.
| -rw-r--r-- | plugin/HISTORY | 21 | ||||
| -rw-r--r-- | plugin/Makefile | 111 | ||||
| -rw-r--r-- | plugin/README | 81 | ||||
| -rw-r--r-- | plugin/README - series | 48 | ||||
| -rw-r--r-- | plugin/README - vps | 33 | ||||
| -rw-r--r-- | plugin/TODO | 13 | ||||
| -rw-r--r-- | plugin/vdrtva.c | 964 | ||||
| -rw-r--r-- | plugin/vdrtva.h | 95 | ||||
| -rw-r--r-- | plugin/vdrvps-1.7.20.diff | 61 |
9 files changed, 1427 insertions, 0 deletions
diff --git a/plugin/HISTORY b/plugin/HISTORY new file mode 100644 index 0000000..b6f70dc --- /dev/null +++ b/plugin/HISTORY @@ -0,0 +1,21 @@ +VDR Plugin 'vdrtva' Revision History +------------------------------------- + +2007-10-07: Version 0.0.1 + +- Initial experimental plugin. + +2011-06-18: Version 0.0.2 + +- First functional proof-of-concept. + +2011-07-17: Version 0.0.3 + +- Handling of Links file moved from Perl script into plugin. +- Implemented config settings. + +2011-08-18: Version 0.0.4 + +- Major functions of Perl script built into plugin. +- Auto-execution of series link updates. +- Simplified internal data structures. diff --git a/plugin/Makefile b/plugin/Makefile new file mode 100644 index 0000000..421a70e --- /dev/null +++ b/plugin/Makefile @@ -0,0 +1,111 @@ +# +# Makefile for a Video Disk Recorder plugin +# +# $Id$ + +# The official name of this plugin. +# This name will be used in the '-P...' option of VDR to load the plugin. +# By default the main source file also carries this name. +# IMPORTANT: the presence of this macro is important for the Make.config +# file. So it must be defined, even if it is not used here! +# +PLUGIN = vdrtva + +### The version number of this plugin (taken from the main source file): + +VERSION = $(shell grep 'static const char \*VERSION *=' $(PLUGIN).c | awk '{ print $$6 }' | sed -e 's/[";]//g') + +### The C++ compiler and options: + +CXX ?= g++ +CXXFLAGS ?= -g -O3 -Wall -Woverloaded-virtual -Wno-parentheses + +### The directory environment: + +VDRDIR = ../../.. +LIBDIR = ../../lib +TMPDIR = /tmp + +### Make sure that necessary options are included: + +include $(VDRDIR)/Make.global + +### Allow user defined options to overwrite defaults: + +-include $(VDRDIR)/Make.config + +### The version number of VDR's plugin API (taken from VDR's "config.h"): + +APIVERSION = $(shell sed -ne '/define APIVERSION/s/^.*"\(.*\)".*$$/\1/p' $(VDRDIR)/config.h) + +### The name of the distribution archive: + +ARCHIVE = $(PLUGIN)-$(VERSION) +PACKAGE = vdr-$(ARCHIVE) + +### Includes and Defines (add further entries here): + +INCLUDES += -I$(VDRDIR)/include + +DEFINES += -D_GNU_SOURCE -DPLUGIN_NAME_I18N='"$(PLUGIN)"' + +### The object files (add further files here): + +OBJS = $(PLUGIN).o + +### Implicit rules: + +%.o: %.c + $(CXX) $(CXXFLAGS) -c $(DEFINES) $(INCLUDES) $< + +### Dependencies: + +MAKEDEP = $(CXX) -MM -MG +DEPFILE = .dependencies +$(DEPFILE): Makefile + @$(MAKEDEP) $(DEFINES) $(INCLUDES) $(OBJS:%.o=%.c) > $@ + +-include $(DEPFILE) + +### Internationalization (I18N): + +PODIR = po +LOCALEDIR = $(VDRDIR)/locale +I18Npo = $(wildcard $(PODIR)/*.po) +I18Ndirs = $(notdir $(foreach file, $(I18Npo), $(basename $(file)))) +I18Npot = $(PODIR)/$(PLUGIN).pot + +%.mo: %.po + msgfmt -c -o $@ $< + +$(I18Npot): $(wildcard *.c) + xgettext -C -cTRANSLATORS --no-wrap -F -k -ktr -ktrNOOP --msgid-bugs-address='<see README>' -o $@ $(wildcard *.c) + +$(I18Npo): $(I18Npot) + msgmerge -U --no-wrap -F --backup=none -q $@ $< + +i18n: $(I18Nmo) + @mkdir -p $(LOCALEDIR) + for i in $(I18Ndirs); do\ + mkdir -p $(LOCALEDIR)/$$i/LC_MESSAGES;\ + cp $(PODIR)/$$i.mo $(LOCALEDIR)/$$i/LC_MESSAGES/$(PLUGIN).mo;\ + done + +### Targets: + +all: libvdr-$(PLUGIN).so i18n + +libvdr-$(PLUGIN).so: $(OBJS) + $(CXX) $(CXXFLAGS) -shared $(OBJS) -o $@ + @cp --remove-destination $@ $(LIBDIR)/$@.$(APIVERSION) + +dist: clean + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @mkdir $(TMPDIR)/$(ARCHIVE) + @cp -a * $(TMPDIR)/$(ARCHIVE) + @tar czf $(PACKAGE).tgz -C $(TMPDIR) $(ARCHIVE) + @-rm -rf $(TMPDIR)/$(ARCHIVE) + @echo Distribution package created as $(PACKAGE).tgz + +clean: + @-rm -f $(OBJS) $(DEPFILE) *.so *.tgz core* *~ $(PODIR)/*.mo $(PODIR)/*.pot diff --git a/plugin/README b/plugin/README new file mode 100644 index 0000000..37cd204 --- /dev/null +++ b/plugin/README @@ -0,0 +1,81 @@ +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. See the file COPYING for more information. + +Description: + +TV-Anytime is the name given to a set of technologies which aim to simplify the +process of recording and replaying broadcast content. The standards are +published by ETSI and are available without cost from www.etsi.org. The main +standard is ETSI TS 102 323. + +In the UK a subset of the TV-Anytime specification is broadcast on the DTV +service under the trade name "FreeView Plus". This patch is written for the UK +version but should work with the full specification (untested). + +TV-Anytime data is contained in Content Reference Identifiers (CRIDs). The +syntax of a CRID is described in RFC 4078; it is a URI-compliant string of the +form: + + crid://<DNS name>/<data> + +in which <DNS name> is a registered internet domain name (RFC 1034) and <data> +is a free-format string. The <DNS Name> section relates to the content provider +(TV channel or company), and the <data> section to the programme. + +CRIDs are transmitted in the EIT as Content Identifier Descriptors, with +descriptor ID 0x76. To save bandwith only the <data> section is sent, the <DNS +Name> part is taken from the Default Authority Descriptor in the SDT, and the +crid:// is assumed. + +A programme may have up to two CRIDs in its EPG entry. One identifies the +specific item of content which is being broadcast, while the other identifies a +series of programmes which this item belongs to. In FreeView Plus these CRIDs +have crid_type values 0x31 and 0x32 respectively (TV-Anytime uses values 0x01 +and 0x02). + +To give an example, the programme "Torchwood" broadcast on channel BBC2 at 21:00 +on 2008-01-16 had item CRID '54BXLC' and series CRID 'KCJ00C'. When the same +programme was repeated the following day on channel BBC3, the item CRID remained +the same but the series CRID was 'KCJ12C'. Meanwhile the episode broadcast on +BBC2 one week later on 2008-01-24 had CRID '54BXLD' but the same series as the +previous week. Hence it is possible for a PVR to record an entire series by +using the series CRID, or to find an alternative broadcast for an individual +item if there is a clash with another recording. + +Operation: + +The use of the 'Accurate Recording' feature is described in README-vps. + +The plugin runs every 24 hours at a time set by the '-u' parameter (default +03:00). It captures CRID data for a time (10 minutes) then: + +- Checks each series link to see if any new events have been added to the EPG in + the same series. If so then timers are added for them. + +- Checks for new manually-created timers and adds series links for them. + +- Checks for timer clashes and suggests possible alternative recording times. + +- Checks that the event being recorded by each timer is the same as when the + timer was set (ie that the EPG has not changed in the meantime) + +The plugin logs its activity through the VDR syslog. + +The plugin has an SVDRP interface which is mainly used for debugging, but could +be used to interface with other applications. The commands are: + +LSTL Print the series links list + +LSTY Print the CRIDs for each event + +LSTZ Print the Default Authority data for each channel + +STOP Start and stop CRID data capture +STRT + +UPDT Trigger an update of the series links. + + +This is Alpha-quality code - USE AT YOUR OWN RISK!! diff --git a/plugin/README - series b/plugin/README - series new file mode 100644 index 0000000..1450945 --- /dev/null +++ b/plugin/README - series @@ -0,0 +1,48 @@ +This is a very simple script to demonstrate the 'series link' concept. Run it +every day as a cron job and never miss your favourite series again! Just create +a timer for the first programme in a series and the script will automatically +record the rest for you. + +Configuration parameters at the start of the file must be set to match your vdr +settings. The 'padding' values must match those used when you manually set +timers (eg when using vdradmin). + +If you set the 'VPS' config parameter to '1', and also set 'UseVps = 1' and +'VpsFallack=1' in VDR's setup.conf, new timers will be created to use the EIT +Running Status to set start and end times. An accurate Running Status is a +requirement of the Freeview Plus specification. However if the programme starts +earlier than (scheduled time - VpsMargin) the beginning will be missed. + +The script detects split events, eg a film with a news summary in the middle, +and ensures that if a timer is set for one part, all parts of the programme are +recorded (as separate timers). The script also checks for timer clashes, though +it doesn't try to resolve the clash, and also warns if the title of an event in +the EPG has changed since the timer was set (so perhaps a different programme is +being broadcast). + +The script creates a file "links.data" in the vdr directory when run. This file +contains series CRIDs of all of the timers which have been set, and the item +CRIDs of the individual programmes which have had recordings scheduled. A +timestamp against each entry gives the date of the last timer set, so that old +series can be automatically purged. + +Points to remember: + +- Not all channels on UK Freeview have CRIDs in the EPG (at present just the + BBC, ITV, C4 and C5 stables plus Sky 3 and Virgin 1). Some radio channels + have item CRIDs but none have series CRIDs. + +- Different programme providers have different ideas of what constitutes a + 'series'. + +- The timer creation process is very simplistic; it doesn't check for timer + clashes, and selects the first physical entry in the EPG (which may not be + the prime broadcast of the programme). + +- A series link is created for every timer whether you want one or not. + +- If you run this script overnight, a timer set one day which fires on the same + day will not create a series link (because the timer no longer exists). + +- This script has not been tested with multiple tuner cards or with mixed DVB-T + and DVB-S setups. diff --git a/plugin/README - vps b/plugin/README - vps new file mode 100644 index 0000000..8d172c2 --- /dev/null +++ b/plugin/README - vps @@ -0,0 +1,33 @@ +One of the important features of a video recorder is the ability to deal with +programme schedule changes and to ensure that the wanted programme is always +recorded. TVAnytime has its own set of data descriptors to handle late-running +or cancelled programmes but these are not used by Freeview Plus. Instead the +Running Status from the now/next section of the EIT is used to start and stop +the recording. + +VDR can control timers from the running status by using 'VPS' functions in +conjunction with this patch. Note however that the normal VPS operation will no +longer work. + +To use VPS for accurate recording, some conditions must be met: + +- VDR must be compiled with the 'VPS Fallback' patch (included with this + plugin). Note that some Linux distributions (esp. Mandriva) include this + patch in their binaries. +- Parameters 'UseVps' and 'VpsFallback' in setup.conf must both be set to 1 +- Timers must have the 'use VPS' flag set +- The start time of a timer must be set to exactly the scheduled start time of + the programme (ie no padding). + +With these conditions met, the sequence of events is: + +- Shortly before the scheduled time of the recording (set by 'VpsMargin' in + VDR's setup.conf) VDR switches to the multiplex containing the channel to be + recorded +- VDR monitors the running status of the programme to be recorded +- When the running status changes to 'running', VDR starts recording +- When the running status changes to 'not running', VDR stops recording and + deletes the timer. + +CAUTION - VPS recordings seem to have a lower priority than live viewing. VDR +will not interrupt live viewing to start looking for the start of a recording.
\ No newline at end of file diff --git a/plugin/TODO b/plugin/TODO new file mode 100644 index 0000000..8e78452 --- /dev/null +++ b/plugin/TODO @@ -0,0 +1,13 @@ +Handle CRID 'suggestions' via new SVDRP command. + +Mail a log file after the update. + +Missing functions from Perl script: + Check split recordings + +Timer clash check code needs to cope with multiple tuner cards. + +Maybe add a timer callback so that a series can be registered immediately +instead of waiting for an overnight update. + +Config file diff --git a/plugin/vdrtva.c b/plugin/vdrtva.c new file mode 100644 index 0000000..9c7dee6 --- /dev/null +++ b/plugin/vdrtva.c @@ -0,0 +1,964 @@ +/* + * vdrtva.c: A plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <vdr/plugin.h> +#include <libsi/section.h> +#include <libsi/descriptor.h> +#include <stdarg.h> +#include <getopt.h> + +#include "vdrtva.h" + +#define SVDRPOSD_BUFSIZE KILOBYTE(4) +#define SECONDSPERDAY (86400) + +cChanDAs *ChanDAs; +cEventCRIDs *EventCRIDs; +cLinks *Links; + +static const char *VERSION = "0.0.4"; +static const char *DESCRIPTION = "TV-Anytime plugin"; +static const char *MAINMENUENTRY = "vdrTva"; + +class cPluginvdrTva : public cPlugin { +private: + // Add any member variables or functions you may need here. + int length; + int size; + int seriesLifetime; + int priority; + int lifetime; + int flags; + int collectionperiod; + int state; + time_t nextactiontime; + int updatehours; + int updatemins; + char *buffer; + char* configDir; + cTvaFilter *Filter; + bool Append(const char *Fmt, ...); + bool AppendItems(const char* Option); + const char* Reply(); + bool AddSeriesLink(const char *scrid, int modtime, const char *icrid); + void LoadLinksFile(void); + bool SaveLinksFile(void); + bool UpdateLinksFromTimers(void); + bool AddNewEventsToSeries(void); + void CheckChangedEvents(void); + void CheckTimerClashes(void); + void FindAlternatives(const cEvent *event); + void StartDataCapture(void); + void StopDataCapture(void); + void Update(void); + void Check(void); +// void Reset(); +public: + cPluginvdrTva(void); + virtual ~cPluginvdrTva(); + virtual const char *Version(void) { return VERSION; } + virtual const char *Description(void) { return tr(DESCRIPTION); } + virtual const char *CommandLineHelp(void); + virtual bool ProcessArgs(int argc, char *argv[]); + virtual bool Initialize(void); + virtual bool Start(void); + virtual void Stop(void); + virtual void Housekeeping(void); + virtual void MainThreadHook(void); + virtual cString Active(void); + virtual time_t WakeupTime(void); + virtual const char *MainMenuEntry(void) { return MAINMENUENTRY; } + virtual cOsdObject *MainMenuAction(void); + virtual cMenuSetupPage *SetupMenu(void); + virtual bool SetupParse(const char *Name, const char *Value); + virtual bool Service(const char *Id, void *Data = NULL); + virtual const char **SVDRPHelpPages(void); + virtual cString SVDRPCommand(const char *Command, const char *Option, int &ReplyCode); + }; + +cPluginvdrTva::cPluginvdrTva(void) +{ + // Initialize any member variables here. + // DON'T DO ANYTHING ELSE THAT MAY HAVE SIDE EFFECTS, REQUIRE GLOBAL + // VDR OBJECTS TO EXIST OR PRODUCE ANY OUTPUT! + buffer = NULL; + configDir = NULL; + Filter = NULL; + ChanDAs = NULL; + EventCRIDs = NULL; + Links = NULL; + seriesLifetime = 30 * SECONDSPERDAY; + priority = 99; + lifetime = 99; + flags = 5; + state = 0; + collectionperiod = 10 * 60; + updatehours = 3; + updatemins = 0; +} + +cPluginvdrTva::~cPluginvdrTva() +{ + // Clean up after yourself! +} + +const char *cPluginvdrTva::CommandLineHelp(void) +{ + // Return a string that describes all known command line options. + return " -l n --lifetime=n Lifetime of new timers (default 99)\n" + " -p n --priority=n Priority of new timers (default 99)\n" + " -s n --serieslifetime=n Days to remember a series after the last event (default 30)\n" + " -u HH:MM --updatetime=HH:MM Time to update series links (default 03:00)\n"; +} + +bool cPluginvdrTva::ProcessArgs(int argc, char *argv[]) +{ + // Implement command line argument processing here if applicable. + static struct option long_options[] = { + { "serieslifetime", required_argument, NULL, 's' }, + { "priority", required_argument, NULL, 'p' }, + { "lifetime", required_argument, NULL, 'l' }, + { "updatetime", required_argument, NULL, 'u' }, + { NULL } + }; + + int c, opt; + char *hours, *mins, *strtok_next; + char buf[32]; + while ((c = getopt_long(argc, argv, "l:p:s:u:", long_options, NULL)) != -1) { + switch (c) { + case 'l': + opt = atoi(optarg); + if (opt > 0) lifetime = opt; + break; + case 'p': + opt = atoi(optarg); + if (opt > 0) priority = opt; + break; + case 's': + opt = atoi(optarg); + if (opt > 0) seriesLifetime = opt * SECONDSPERDAY; + break; + case 'u': + strncpy(buf, optarg,sizeof(buf)); + hours = strtok_r(buf, ":", &strtok_next); + mins = strtok_r(NULL, "!", &strtok_next); + updatehours = atoi(hours); + updatemins = atoi(mins); + break; + default: + return false; + } + } + return true; +} + +bool cPluginvdrTva::Initialize(void) +{ + // Initialize any background activities the plugin shall perform. + return true; +} + +bool cPluginvdrTva::Start(void) +{ + // Start any background activities the plugin shall perform. + configDir = strcpyrealloc(configDir, cPlugin::ConfigDirectory("vdrtva")); + LoadLinksFile(); + + struct tm tm_r; + char buff[32]; + time_t now = time(NULL); + localtime_r(&now, &tm_r); + tm_r.tm_sec = 0; + tm_r.tm_hour = updatehours; + tm_r.tm_min = updatemins; + nextactiontime = mktime(&tm_r); + if (nextactiontime < now) nextactiontime += SECONDSPERDAY; + ctime_r(&nextactiontime, buff); + isyslog("vdrtva: next update due at %s", buff); + return true; +} + +void cPluginvdrTva::Stop(void) +{ + // Stop any background activities the plugin is performing. + if (Filter) { + delete Filter; + Filter = NULL; + } +} + +void cPluginvdrTva::Housekeeping(void) +{ + // Perform any cleanup or other regular tasks. + if (nextactiontime < time(NULL)) { + switch (state) { + case 0: + StartDataCapture(); + nextactiontime += collectionperiod; + state++; + break; + case 1: + StopDataCapture(); + state++; + break; + case 2: + Update(); + state++; + break; + case 3: + Check(); + nextactiontime += (SECONDSPERDAY - collectionperiod); + state = 0; + break; + } + } +} + +void cPluginvdrTva::MainThreadHook(void) +{ + // Perform actions in the context of the main program thread. + // WARNING: Use with great care - see PLUGINS.html! +} + +cString cPluginvdrTva::Active(void) +{ + // Return a message string if shutdown should be postponed + return NULL; +} + +time_t cPluginvdrTva::WakeupTime(void) +{ + // Return custom wakeup time for shutdown script + return 0; +} + +cOsdObject *cPluginvdrTva::MainMenuAction(void) +{ + // Perform the action when selected from the main VDR menu. + return NULL; +} + +cMenuSetupPage *cPluginvdrTva::SetupMenu(void) +{ + // Return a setup menu in case the plugin supports one. + return NULL; +} + +bool cPluginvdrTva::SetupParse(const char *Name, const char *Value) +{ + // Parse your own setup parameters and store their values. + return false; +} + +bool cPluginvdrTva::Service(const char *Id, void *Data) +{ + // Handle custom service requests from other plugins + return false; +} + +const char **cPluginvdrTva::SVDRPHelpPages(void) +{ + // Return help text for SVDRP commands this plugin implements + static const char *HelpPages[] = { + "LSTL\n" + " Print the Links list.", + "LSTY\n" + " Print the Event list including CRIDs.", + "LSTZ\n" + " Print the channel list with Default Authority.", + "STOP\n" + " Stop Event data capture (retaining data).", + "STRT\n" + " Start Event data capture (erasing any existing data)", + "UPDT\n" + " Update timers and links (series link functionality)", + NULL + }; + return HelpPages; +} + +cString cPluginvdrTva::SVDRPCommand(const char *Command, const char *Option, int &ReplyCode) +{ + // Process SVDRP commands this plugin implements + isyslog ("vdrtva: processing command %s", Command); + if (strcasecmp(Command, "LSTL") == 0) { + if (Links && (Links->MaxNumber() >=1)) { + ReplyCode = 250; + for (cLinkItem *linkItem = Links->First(); linkItem; linkItem = Links->Next(linkItem)) { + Append("%s,%d;%s\n", linkItem->sCRID(), linkItem->ModTime(), linkItem->iCRIDs()); + } + } + if (buffer && length > 0) return cString(Reply(), true); + else return cString::sprintf("Nothing in the buffer!"); + } + else if (strcasecmp(Command, "LSTY") == 0) { + if (EventCRIDs && (EventCRIDs->MaxNumber() >= 1)) { + ReplyCode = 250; + for (cEventCRID *eventCRID = EventCRIDs->First(); eventCRID; eventCRID = EventCRIDs->Next(eventCRID)) { + cChanDA *chanDA = ChanDAs->GetByChannelID(eventCRID->Cid()); + if(chanDA) { + Append("%d %d %s%s %s%s\n", chanDA->Cid(), eventCRID->Eid(), chanDA->DA(), eventCRID->iCRID(), chanDA->DA(), eventCRID->sCRID()); + } + } + if (buffer && length > 0) return cString(Reply(), true); + else return cString::sprintf("Nothing in the buffer!"); + } + else + return cString::sprintf("No events defined"); + } + else if (strcasecmp(Command, "LSTZ") == 0) { + if (ChanDAs && (ChanDAs->MaxNumber() >= 1)) { + ReplyCode = 250; + for (cChanDA *chanDA = ChanDAs->First(); chanDA; chanDA = ChanDAs->Next(chanDA)) { + Append("%d %s\n", chanDA->Cid(), chanDA->DA()); + } + if (buffer && length > 0) return cString(Reply(), true); + else return cString::sprintf("Nothing in the buffer!"); + } + else + return cString::sprintf("No channels defined"); + } + else if (strcasecmp(Command, "STRT") == 0) { + if (!Filter) { + StartDataCapture(); + return cString::sprintf("Data capture started"); + } + else { + ReplyCode = 999; + return cString::sprintf("Double start attempted"); + } + } + else if (strcasecmp(Command, "STOP") == 0) { + if (Filter) { + StopDataCapture(); + return cString::sprintf("Data capture stopped"); + } + else { + ReplyCode = 999; + return cString::sprintf("Double stop attempted"); + } + } + else if (strcasecmp(Command, "UPDT") == 0) { + Update(); + Check(); + return cString::sprintf("Update completed"); + } + return NULL; +} + +bool cPluginvdrTva::Append(const char *Fmt, ...) +{ + va_list ap; + + if (!buffer) { + length = 0; + size = SVDRPOSD_BUFSIZE; + buffer = (char *) malloc(sizeof(char) * size); + } + while (buffer) { + va_start(ap, Fmt); + int n = vsnprintf(buffer + length, size - length, Fmt, ap); + va_end(ap); + + if (n < size - length) { + length += n; + return true; + } + // overflow: realloc and try again + size += SVDRPOSD_BUFSIZE; + char *tmp = (char *) realloc(buffer, sizeof(char) * size); + if (!tmp) + free(buffer); + buffer = tmp; + } + return false; +} + +const char* cPluginvdrTva::Reply() +{ + char *tmp = buffer; + buffer = NULL; + return tmp; +} + +void cPluginvdrTva::StartDataCapture() +{ + if (!Filter) { + if (EventCRIDs) delete EventCRIDs; + if (ChanDAs) delete ChanDAs; + EventCRIDs = new cEventCRIDs(); + ChanDAs = new cChanDAs(); + Filter = new cTvaFilter(); + cDevice::ActualDevice()->AttachFilter(Filter); + isyslog("vdrtva: Data capture started"); + } +} + +void cPluginvdrTva::StopDataCapture() +{ + if (Filter) { + delete Filter; + Filter = NULL; + isyslog("vdrtva: Data capture stopped"); + } +} + +void cPluginvdrTva::Update() +{ + if( + UpdateLinksFromTimers() || + AddNewEventsToSeries() + ) SaveLinksFile(); + isyslog("vdrtva: Updates complete"); +} + +void cPluginvdrTva::Check() +{ + CheckChangedEvents(); + CheckTimerClashes(); + isyslog("vdrtva: Checks complete"); +} + +// add a new event to the Links table, either as an addition to an existing series or as a new series. +// return false = nothing done, true = new event for old series, or new series. + +bool cPluginvdrTva::AddSeriesLink(const char *scrid, int modtime, const char *icrid) +{ + if (Links && (Links->MaxNumber() >=1)) { + for (cLinkItem *Item = Links->First(); Item; Item = Links->Next(Item)) { + if (strcasecmp(Item->sCRID(), scrid) == 0) { + if (strstr(Item->iCRIDs(), icrid) == NULL) { + cString icrids = cString::sprintf("%s:%s", Item->iCRIDs(), icrid); + modtime = max(Item->ModTime(), modtime); + Item->Set(Item->sCRID(), modtime, icrids); + isyslog("vdrtva: Adding new event %s to series %s\n", icrid, scrid); + return true; + } + return false; + } + } + } + Links->NewLinkItem(scrid, modtime, icrid); + isyslog("vdrtva: Creating new series %s for event %s\n", scrid, icrid); + return true; +} + +void cPluginvdrTva::LoadLinksFile() +{ + Links = new cLinks(); + cString curlinks = AddDirectory(configDir, "links.data"); + FILE *f = fopen(curlinks, "r"); + if (f) { + char *s; + char *strtok_next; + cReadLine ReadLine; + cLinkItem *LinkItem; + int modtime; + while ((s = ReadLine.Read(f)) != NULL) { + char *scrid = strtok_r(s, ",", &strtok_next); + char *mtime = strtok_r(NULL, ";", &strtok_next); + char *icrids = strtok_r(NULL, "!", &strtok_next); + modtime = atoi(mtime); + LinkItem = Links->NewLinkItem(scrid, modtime, icrids); + } + fclose (f); + isyslog("vdrtva: loaded %d series links\n", Links->MaxNumber()); + } + else isyslog("vdrtva: series links file not found\n"); +} + +bool cPluginvdrTva::SaveLinksFile() +{ + cString curlinks = AddDirectory(configDir, "links.data"); + cString newlinks = AddDirectory(configDir, "links.new"); + cString oldlinks = AddDirectory(configDir, "links.old"); + FILE *f = fopen(newlinks, "w"); + if (f) { + for (cLinkItem *Item = Links->First(); Item; Item = Links->Next(Item)) { + if ((Item->ModTime() + seriesLifetime) > time(NULL)) { + fprintf(f, "%s,%d;%s\n", Item->sCRID(), Item->ModTime(), Item->iCRIDs()); + } + else { + isyslog ("vdrtva: Expiring series %s\n", Item->sCRID()); + Links->Del(Item); + } + } + fclose(f); + unlink (oldlinks); // Allow to fail if the save file does not exist + rename (curlinks, oldlinks); + rename (newlinks, curlinks); + } + return true; +} + +// Check that all timers are part of series links and update the links. + +bool cPluginvdrTva::UpdateLinksFromTimers() +{ + if ((Timers.Count() == 0) || (!EventCRIDs)) return false; + bool status = false; + for (int i = 0; i < Timers.Count(); i++) { + cTimer *timer = Timers.Get(i); + if (timer) { +// find the event for this timer + const cEvent *event = timer->Event(); + if (event) { + cChannel *channel = Channels.GetByChannelID(event->ChannelID()); +// find the sCRID and iCRID for the event + cChanDA *chanda = ChanDAs->GetByChannelID(channel->Number()); + cEventCRID *eventcrid = EventCRIDs->GetByID(channel->Number(), event->EventID()); + if (eventcrid && chanda) { + cString scrid = cString::sprintf("%s%s", chanda->DA(),eventcrid->sCRID()); + cString icrid = cString::sprintf("%s%s", chanda->DA(),eventcrid->iCRID()); +// scan the links table for the sCRID +// if found, check if the iCRID is present, if not add it +// else create a new links entry + status |= AddSeriesLink(scrid, event->StartTime(), icrid); + } + } + } + } + return status; +} + +// Find new events for series links and create timers for them. +// It would be simpler to create the timer directly from the event, but it would not then be possible to +// control the VPS parameters. + +bool cPluginvdrTva::AddNewEventsToSeries() +{ + bool saveNeeded = false; + if (!Links || (Links->MaxNumber() < 1)) return false; +// Foreach CRID + for (cEventCRID *eventCRID = EventCRIDs->First(); eventCRID; eventCRID = EventCRIDs->Next(eventCRID)) { + cChanDA *chanDA = ChanDAs->GetByChannelID(eventCRID->Cid()); + if (chanDA) { +// Check for an entry in the Links table with the same sCRID + cString scrid = cString::sprintf("%s%s", chanDA->DA(),eventCRID->sCRID()); + for (cLinkItem *Item = Links->First(); Item; Item = Links->Next(Item)) { + if (strcasecmp(Item->sCRID(), scrid) == 0) { +// if found, look for the event's icrid in ALL series + cString icrid = cString::sprintf("%s%s", chanDA->DA(),eventCRID->iCRID()); + bool done = false; + for (cLinkItem *Item2 = Links->First(); Item2; Item2 = Links->Next(Item2)) { + if (strstr(Item2->iCRIDs(), icrid) != NULL) { + done = true; + } + } +// if not found, add a new timer for the event and update the series. + if (!done) { + cChannel *channel = Channels.GetByNumber(eventCRID->Cid()); + cSchedulesLock SchedulesLock; + const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock); + if (Schedules) { + const cSchedule *schedule = Schedules->GetSchedule(channel); + if (schedule) { + const cEvent *event = schedule->GetEvent(eventCRID->Eid()); + struct tm tm_r; + char startbuff[64], endbuff[64]; + time_t starttime = event->StartTime(); + time_t endtime = event->EndTime(); + if (!Setup.UseVps) { + starttime -= Setup.MarginStart; + endtime += Setup.MarginStop; + } + localtime_r(&starttime, &tm_r); + strftime(startbuff, sizeof(startbuff), "%Y-%m-%d:%H%M", &tm_r); + localtime_r(&endtime, &tm_r); + strftime(endbuff, sizeof(endbuff), "%H%M", &tm_r); + cString timercmd = cString::sprintf("%u:%d:%s:%s:%d:%d:%s:\n", flags, channel->Number(), startbuff, endbuff, priority, lifetime, event->Title()); + cTimer *timer = new cTimer; + if (timer->Parse(timercmd)) { + cTimer *t = Timers.GetTimer(timer); + if (!t) { + Timers.Add(timer); + Timers.SetModified(); + isyslog("vdrtva: timer %s added", *timer->ToDescr()); + AddSeriesLink(scrid, event->StartTime(), icrid); + saveNeeded = true; + } + else isyslog("vdrtva: Duplicate timer creation attempted for %s", *timer->ToDescr()); + } + } + } + } + } + } + } + } + return saveNeeded; +} + +// Check timers to see if the event they were set to record is still in the EPG. +// This won't work if the start time is padded. + +void cPluginvdrTva::CheckChangedEvents() +{ + if (Timers.Count() == 0) return; + for (int i = 0; i < Timers.Count(); i++) { + cTimer *timer = Timers.Get(i); + if (timer) { + const cChannel *channel = timer->Channel(); + cSchedulesLock SchedulesLock; + const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock); + if (Schedules) { + const cSchedule *schedule = Schedules->GetSchedule(channel); + if (schedule) { + const cEvent *event = schedule->GetEvent(NULL, timer->StartTime()); + if (!event) isyslog("Event for timer '%s' seems to no longer exist", timer->File()); + else if (strcmp(timer->File(), event->Title())) { + isyslog("vdrtva: Changed timer event: %s <=> %s", timer->File(), event->Title()); + } + } + } + } + } +} + +void cPluginvdrTva::CheckTimerClashes(void) +{ + if (Timers.Count() < 2) return; + for (int i = 1; i < Timers.Count(); i++) { + cTimer *timer1 = Timers.Get(i); + if (timer1) { + for (int j = 0; j < i; j++) { + cTimer *timer2 = Timers.Get(j); + if (timer2) { + if((timer1->StartTime() >= timer2->StartTime() && timer1->StartTime() < timer2->StopTime()) + ||(timer2->StartTime() >= timer1->StartTime() && timer2->StartTime() < timer1->StopTime())) { + const cChannel *channel1 = timer1->Channel(); + const cChannel *channel2 = timer2->Channel(); + if (channel1->Transponder() != channel2->Transponder()) { + isyslog("vdrtva: Collision at %s. %s <=> %s", *DayDateTime(timer1->StartTime()), timer1->File(), timer2->File()); + FindAlternatives(timer1->Event()); + FindAlternatives(timer2->Event()); + } + } + } + } + } + } +} + +void cPluginvdrTva::FindAlternatives(const cEvent *event) +{ + if (!event) return; + cChannel *channel = Channels.GetByChannelID(event->ChannelID()); + cChanDA *chanda = ChanDAs->GetByChannelID(channel->Number()); + cEventCRID *eventcrid = EventCRIDs->GetByID(channel->Number(), event->EventID()); + if (!eventcrid || !chanda) return; + + bool found = false; + for (cEventCRID *eventcrid2 = EventCRIDs->First(); eventcrid2; eventcrid2 = EventCRIDs->Next(eventcrid2)) { + if ((strcmp(eventcrid->iCRID(), eventcrid2->iCRID()) == 0) && (event->EventID() != eventcrid2->Eid())) { + cChanDA *chanda2 = ChanDAs->GetByChannelID(eventcrid2->Cid()); + if (strcmp(chanda->DA(), chanda2->DA()) == 0) { + cChannel *channel2 = Channels.GetByNumber(eventcrid2->Cid()); + cSchedulesLock SchedulesLock; + const cSchedules *schedules = cSchedules::Schedules(SchedulesLock); + if (schedules) { + const cSchedule *schedule = schedules->GetSchedule(channel2); + if (schedule) { + const cEvent *event2 = schedule->GetEvent(eventcrid2->Eid(), 0); + if (!found) { + isyslog("vdrtva: Alternatives for '%s':", event->Title()); + found = true; + } + isyslog("vdrtva: %s %s", channel2->Name(), *DayDateTime(event2->StartTime())); + } + } + } + } + } + if (!found) isyslog("vdrtva: No alternatives for '%s':", event->Title()); +} + + +/* + cTvaFilter - capture the CRID data from EIT. +*/ + +cTvaFilter::cTvaFilter(void) +{ + Set(0x11, 0x42); // SDT (Actual) + Set(0x11, 0x46); // SDT (Other) + Set(0x12, 0x40, 0xC0); // event info, actual(0x4E)/other(0x4F) TS, present/following + // event info, actual TS, schedule(0x50)/schedule for future days(0x5X) + // event info, other TS, schedule(0x60)/schedule for future days(0x6X) +} + +void cTvaFilter::Process(u_short Pid, u_char Tid, const u_char *Data, int Length) +{ + // do something with the data here + switch (Pid) { + case 0x11: { + sectionSyncer.Reset(); + SI::SDT sdt(Data, false); + if (!sdt.CheckCRCAndParse()) { + dsyslog ("vdrtva: SDT Parse / CRC error\n"); + return; + } + if (!sectionSyncer.Sync(sdt.getVersionNumber(), sdt.getSectionNumber(), sdt.getLastSectionNumber())) { + dsyslog ("vdrtva: SDT Syncer error\n"); + return; + } + SI::SDT::Service SiSdtService; + for (SI::Loop::Iterator it; sdt.serviceLoop.getNext(SiSdtService, it); ) { + cChannel *chan = Channels.GetByChannelID(tChannelID(Source(),sdt.getOriginalNetworkId(),sdt.getTransportStreamId(),SiSdtService.getServiceId())); + if (!chan) { + return; + } + cChanDA *chanDA = ChanDAs->GetByChannelID(chan->Number()); + if (!chanDA) { + SI::Descriptor *d; + for (SI::Loop::Iterator it2; (d = SiSdtService.serviceDescriptors.getNext(it2)); ) { + switch (d->getDescriptorTag()) { + case SI::DefaultAuthorityDescriptorTag: { + SI::DefaultAuthorityDescriptor *da = (SI::DefaultAuthorityDescriptor *)d; + char DaBuf[Utf8BufSize(1024)]; + da->DefaultAuthority.getText(DaBuf, sizeof(DaBuf)); + chanDA = ChanDAs->NewChanDA(chan->Number()); + chanDA->SetDA(DaBuf); + } + break; + default: ; + } + delete d; + } + } + } + } + case 0x12: { + if (Tid >= 0x4E && Tid <= 0x6F) { +// sectionSyncer.Reset(); + SI::EIT eit(Data, false); + if (!eit.CheckCRCAndParse()) { + dsyslog ("vdrtva: EIT Parse / CRC error\n"); + return; + } + + cChannel *chan = Channels.GetByChannelID(tChannelID(Source(),eit.getOriginalNetworkId(),eit.getTransportStreamId(),eit.getServiceId())); + if (!chan) { + return; + } + SI::EIT::Event SiEitEvent; + for (SI::Loop::Iterator it; eit.eventLoop.getNext(SiEitEvent, it); ) { + cEventCRID *eventCRID = EventCRIDs->GetByID(chan->Number(), SiEitEvent.getEventId()); + if (!eventCRID) { + SI::Descriptor *d; + char iCRIDBuf[Utf8BufSize(1024)] = {'\0'}, sCRIDBuf[Utf8BufSize(1024)] = {'\0'}; + for (SI::Loop::Iterator it2; (d = SiEitEvent.eventDescriptors.getNext(it2)); ) { + switch (d->getDescriptorTag()) { + case SI::ContentIdentifierDescriptorTag: { + SI::ContentIdentifierDescriptor *cd = (SI::ContentIdentifierDescriptor *)d; + SI::ContentIdentifierDescriptor::Identifier cde; + for (SI::Loop::Iterator ite; (cd->identifierLoop.getNext(cde,ite)); ) { + if (cde.getCridLocation() == 0) { + switch (cde.getCridType()) { + case 0x01: // ETSI 102 363 code + case 0x31: // UK Freeview private code + cde.identifier.getText(iCRIDBuf, sizeof(iCRIDBuf)); + break; + case 0x02: // ETSI 102 363 code + case 0x32: // UK Freeview private code + cde.identifier.getText(sCRIDBuf, sizeof(sCRIDBuf)); + // ETSI 102 323 defines CRID type 0x03, which describes 'related' or 'suggested' events. + // Freeview broadcasts these as CRID type 0x33. + // There can be more than one type 0x33 descriptor per event (each with one CRID). + } + } + else { + dsyslog ("vdrtva: Incorrect CRID Loc %x\n", cde.getCridLocation()); + } + } + } + break; + default: ; + } + delete d; + } + if (iCRIDBuf[0] && sCRIDBuf[0]) { // Only log events which are part of a series. + eventCRID = EventCRIDs->NewEventCRID(chan->Number(), SiEitEvent.getEventId()); + eventCRID->SetCRIDs(iCRIDBuf, sCRIDBuf); + } + } + } + } + } + break; + } +} + + +/* + cChanDA - Default Authority for a channel. +*/ + +cChanDA::cChanDA(void) +{ + defaultAuthority = NULL; +} + +cChanDA::~cChanDA(void) +{ + free(defaultAuthority); +} + + +void cChanDA::Set(int Cid) { + cid = Cid; +} + +void cChanDA::SetDA(char *DA) { + defaultAuthority = strcpyrealloc(defaultAuthority, DA); +} + +/* + cChanDAs - in-memory list of channels and Default Authorities. +*/ + +cChanDAs::cChanDAs(void) +{ + maxNumber = 0; +} + +cChanDAs::~cChanDAs(void) +{ + chanDAHash.Clear(); +} + +cChanDA *cChanDAs::GetByChannelID(int cid) +{ + cList<cHashObject> *list = chanDAHash.GetList(cid); + if (list) { + for (cHashObject *hobj = list->First(); hobj; hobj = list->Next(hobj)) { + cChanDA *chanDA = (cChanDA *)hobj->Object(); + if (chanDA->Cid() == cid) + return chanDA; + } + } + return NULL; +} + +cChanDA *cChanDAs::NewChanDA(int Cid) +{ + cChanDA *NewChanDA = new cChanDA; + NewChanDA->Set(Cid); + Add(NewChanDA); + chanDAHash.Add(NewChanDA, Cid); + ChanDAs->SetMaxNumber(ChanDAs->MaxNumber()+1); + return NewChanDA; +} + + +/* + cEventCRID - CRIDs for an event. +*/ + +cEventCRID::cEventCRID(void) +{ + iCrid = sCrid = NULL; +} + +cEventCRID::~cEventCRID(void) +{ + free (iCrid); + free (sCrid); +} + +void cEventCRID::Set(int Cid, tEventID Eid) { + eid = Eid; + cid = Cid; +} + +void cEventCRID::SetCRIDs(char *iCRID, char *sCRID) { + iCrid = strcpyrealloc(iCrid, iCRID); + sCrid = strcpyrealloc(sCrid, sCRID); +} + + +/* + cEventCRIDs - in-memory list of events and CRIDs. +*/ + +cEventCRIDs::cEventCRIDs(void) +{ + maxNumber = 0; +} + +cEventCRIDs::~cEventCRIDs(void) +{ + EventCRIDHash.Clear(); +} + +cEventCRID *cEventCRIDs::GetByID(int Cid, tEventID Eid) +{ + cList<cHashObject> *list = EventCRIDHash.GetList(Cid*33000 + Eid); + if (list) { + for (cHashObject *hobj = list->First(); hobj; hobj = list->Next(hobj)) { + cEventCRID *EventCRID = (cEventCRID *)hobj->Object(); + if ((EventCRID->Eid() == Eid) && (EventCRID->Cid() == Cid)) + return EventCRID; + } + } + return NULL; +} + +cEventCRID *cEventCRIDs::NewEventCRID(int Cid, tEventID Eid) +{ + cEventCRID *NewEventCRID = new cEventCRID; + NewEventCRID->Set(Cid, Eid); + Add(NewEventCRID); + EventCRIDHash.Add(NewEventCRID, Eid + Cid*33000); + EventCRIDs->SetMaxNumber(EventCRIDs->MaxNumber()+1); + return NewEventCRID; +} + + +/* + cLinkItem - Entry from the links file +*/ + +cLinkItem::cLinkItem(void) +{ + sCrid = iCrids = NULL; +} + +cLinkItem::~cLinkItem(void) +{ + free(sCrid); + free(iCrids); +} + +void cLinkItem::Set(const char *sCRID, int ModTime, const char *iCRIDs) +{ + sCrid = strcpyrealloc(sCrid, sCRID); + modtime = ModTime; + iCrids = strcpyrealloc(iCrids, iCRIDs); +} + +/* + cLinks - list of cLinkItem entities +*/ + +cLinks::cLinks(void) +{ + maxNumber = 0; +} + +cLinkItem *cLinks::NewLinkItem(const char *sCRID, int ModTime, const char *iCRIDs) +{ + cLinkItem *NewLinkItem = new cLinkItem; + NewLinkItem->Set(sCRID, ModTime, iCRIDs); + Add(NewLinkItem); + Links->SetMaxNumber(Links->MaxNumber()+1); + return NewLinkItem; +} + +VDRPLUGINCREATOR(cPluginvdrTva); // Don't touch this! diff --git a/plugin/vdrtva.h b/plugin/vdrtva.h new file mode 100644 index 0000000..785f846 --- /dev/null +++ b/plugin/vdrtva.h @@ -0,0 +1,95 @@ +#include <vdr/filter.h> +#include <vdr/device.h> + +class cTvaFilter : public cFilter { +private: + cSectionSyncer sectionSyncer; + cPatFilter *patFilter; +protected: + virtual void Process(u_short Pid, u_char Tid, const u_char *Data, int Length); +public: + cTvaFilter(void); +}; + +class cChanDA : public cListObject { + private: + int cid; + char *defaultAuthority; + public: + cChanDA(void); + ~cChanDA(void); + int Cid(void) { return cid; } + void Set(int Cid); + char * DA(void) { return defaultAuthority; } + void SetDA(char *DA); +}; + +class cChanDAs : public cRwLock, public cConfig<cChanDA> { + private: + int maxNumber; + cHash<cChanDA> chanDAHash; + public: + cChanDAs(void); + ~cChanDAs(void); + int MaxNumber(void) { return maxNumber; } + void SetMaxNumber(int number) { maxNumber = number; } + cChanDA *GetByChannelID(int cid); + cChanDA *NewChanDA(int Cid); +}; + + +class cEventCRID : public cListObject { + private: + tEventID eid; + int cid; + char *iCrid; + char *sCrid; + public: + cEventCRID(void); + ~cEventCRID(void); + tEventID Eid(void) { return eid; } + void Set(int Cid, tEventID Eid); + char * iCRID(void) { return iCrid; } + char * sCRID(void) { return sCrid; } + void SetCRIDs(char *iCRID, char *sCRID); + int Cid(void) { return cid; } +}; + +class cEventCRIDs : public cRwLock, public cConfig<cEventCRID> { + private: + int maxNumber; + cHash<cEventCRID> EventCRIDHash; + public: + cEventCRIDs(void); + ~cEventCRIDs(void); + int MaxNumber(void) { return maxNumber; } + void SetMaxNumber(int number) { maxNumber = number; } + cEventCRID *GetByID(int Cid, tEventID Eid); + cEventCRID *NewEventCRID(int Cid, tEventID Eid); +}; + + +class cLinkItem : public cListObject { + private: + char *sCrid; + int modtime; + char *iCrids; + public: + cLinkItem(void); + ~cLinkItem(void); + void Set(const char *sCRID, int ModTime, const char *iCRIDs); + char * iCRIDs(void) { return iCrids; } + char * sCRID(void) { return sCrid; } + int ModTime(void) { return modtime; } +}; + +class cLinks : public cRwLock, public cConfig<cLinkItem> { + private: + int maxNumber; + public: + cLinks(void); +// ~cLinks(void); + int MaxNumber(void) { return maxNumber; } + void SetMaxNumber(int number) { maxNumber = number; } + cLinkItem *NewLinkItem(const char *sCRID, int ModTime, const char *iCRIDs); +}; diff --git a/plugin/vdrvps-1.7.20.diff b/plugin/vdrvps-1.7.20.diff new file mode 100644 index 0000000..8f93683 --- /dev/null +++ b/plugin/vdrvps-1.7.20.diff @@ -0,0 +1,61 @@ +diff -ur vdr-1.7.20/config.c vdr-1.7.20.new/config.c +--- vdr-1.7.20/config.c 2011-06-13 15:41:01.000000000 +0100 ++++ vdr-1.7.20.new/config.c 2011-08-16 18:08:26.000000000 +0100 +@@ -353,6 +353,7 @@ + UseSubtitle = 1; + UseVps = 0; + VpsMargin = 120; ++ VpsFallback = 0; + RecordingDirs = 1; + FoldersInTimerMenu = 1; + NumberKeysForChars = 1; +@@ -545,6 +546,7 @@ + else if (!strcasecmp(Name, "UseSubtitle")) UseSubtitle = atoi(Value); + else if (!strcasecmp(Name, "UseVps")) UseVps = atoi(Value); + else if (!strcasecmp(Name, "VpsMargin")) VpsMargin = atoi(Value); ++ else if (!strcasecmp(Name, "VpsFallback")) VpsFallback = atoi(Value); + else if (!strcasecmp(Name, "RecordingDirs")) RecordingDirs = atoi(Value); + else if (!strcasecmp(Name, "FoldersInTimerMenu")) FoldersInTimerMenu = atoi(Value); + else if (!strcasecmp(Name, "NumberKeysForChars")) NumberKeysForChars = atoi(Value); +@@ -641,6 +643,7 @@ + Store("UseSubtitle", UseSubtitle); + Store("UseVps", UseVps); + Store("VpsMargin", VpsMargin); ++ Store("VpsFallback", VpsFallback); + Store("RecordingDirs", RecordingDirs); + Store("FoldersInTimerMenu", FoldersInTimerMenu); + Store("NumberKeysForChars", NumberKeysForChars); +diff -ur vdr-1.7.20/config.h vdr-1.7.20.new/config.h +--- vdr-1.7.20/config.h 2011-06-21 22:43:01.000000000 +0100 ++++ vdr-1.7.20.new/config.h 2011-08-16 18:08:26.000000000 +0100 +@@ -252,6 +252,7 @@ + int UseSubtitle; + int UseVps; + int VpsMargin; ++ int VpsFallback; + int RecordingDirs; + int FoldersInTimerMenu; + int NumberKeysForChars; +diff -ur vdr-1.7.20/menu.c vdr-1.7.20.new/menu.c +--- vdr-1.7.20/menu.c 2011-08-06 14:13:34.000000000 +0100 ++++ vdr-1.7.20.new/menu.c 2011-08-16 18:08:26.000000000 +0100 +@@ -3058,6 +3058,7 @@ + Add(new cMenuEditBoolItem(tr("Setup.Recording$Use episode name"), &data.UseSubtitle)); + Add(new cMenuEditBoolItem(tr("Setup.Recording$Use VPS"), &data.UseVps)); + Add(new cMenuEditIntItem( tr("Setup.Recording$VPS margin (s)"), &data.VpsMargin, 0)); ++ Add(new cMenuEditBoolItem(tr("Setup.Recording$Use running status as VPS fallback"), &data.VpsFallback)); + Add(new cMenuEditBoolItem(tr("Setup.Recording$Mark instant recording"), &data.MarkInstantRecord)); + Add(new cMenuEditStrItem( tr("Setup.Recording$Name instant recording"), data.NameInstantRecord, sizeof(data.NameInstantRecord))); + Add(new cMenuEditIntItem( tr("Setup.Recording$Instant rec. time (min)"), &data.InstantRecordTime, 1, MAXINSTANTRECTIME)); +diff -ur vdr-1.7.20/timers.c vdr-1.7.20.new/timers.c +--- vdr-1.7.20/timers.c 2011-08-06 14:13:54.000000000 +0100 ++++ vdr-1.7.20.new/timers.c 2011-08-16 18:08:26.000000000 +0100 +@@ -430,7 +430,7 @@ + deferred = 0; + + if (HasFlags(tfActive)) { +- if (HasFlags(tfVps) && event && event->Vps()) { ++ if (HasFlags(tfVps) && event && (Setup.VpsFallback || event->Vps())) { + if (Margin || !Directly) { + startTime = event->StartTime(); + stopTime = event->EndTime(); |
