diff --git a/HISTORY b/HISTORY index 77b4f9af..3fcd8e48 100644 --- a/HISTORY +++ b/HISTORY @@ -9562,3 +9562,9 @@ Video Disk Recorder Revision History - Fixed a compiler warning (thanks to Winfried Khler). - Fixed convertCharacterTable() in case iconv_open() fails (thanks to Helmut Binder). - Official release. + +2020-12-26: Version 2.5.1 + +- Implemented "Pattern Timers" (see MANUAL, vdr.1 and vdr.5 for details). +- Events in the past are no longer marked as having a timer in the Schedules + menu. diff --git a/MANUAL b/MANUAL index 906b065d..cf667665 100644 --- a/MANUAL +++ b/MANUAL @@ -480,6 +480,8 @@ Version 2.4 "forever", and a value of 0 means that this recording can be deleted any time if a recording with a higher priority needs disk space. + Pattern: The pattern to use for recordings matching events (only available + for pattern timers). See section "Pattern timers" below. File: The name under which a recording created through this timer will be stored on disk (the actual name will also contain the date and time, so it is possible to have a "repeating timer" store all its @@ -511,6 +513,129 @@ Version 2.4 The "Red" key in the "Edit timer" menu opens a list of folders, which can be used to define the file name in which the recording will be stored. + The "Yellow" key in the "Edit timer" menu toggles the timer between "Pattern" + and "Regular". + + When editing the "File" field, the "Blue" key in can be used to insert useful + macros. + +* Pattern timers + +There are cases where it would make sense to have a more flexible kind of +recording timer. For instance, some channels that provide VPS don't always +use the exact same VPS time for a series, which is extremely annoying. +Or you might want to record all films that have a certain pattern in their +title, no matter when they are broadcast. In such cases, "pattern timers" +can help. + +In the "Edit timer" menu press the Yellow button to turn a regular timer into +a pattern timer. Pressing this button again switches back to regular. + +The following rules apply to pattern timers: + +- Pattern timers can only work for channels that provide EPG data. +- When using pattern timers, there should always be at least one free device that + can be used to regularly receive the EPG of the pattern timer's channel. +- A pattern timer records every matching event on the given channel that overlaps + with the given start/stop time. Overlapping events are recorded in full, + even if they extend outside the given start/stop interval. +- In order to actually record an event, a pattern timer "spawns" a separate timer + that does the recording. At most two timers are spawned from a pattern timer at + any given time, one for the next upcoming matching event, and one for + the event immediately following that one, in case it also matches. +- Spawned timers are marked with the flag tfSpawned. +- Spawned timers take the Priority, Lifetime and VPS settings from the pattern timer. +- The special pattern "*" matches every event. So a timer with + a start/stop time of 00:00/23:59 will record every event of that day + into separate recordings. Note that when using this pattern there should + be no other timers for the same channel, because these might interfere. +- Once a timer has been spawned, it is treated like any other regular + timer. Any changes made to the corresponding pattern timer thereafter will have + no effect on spawned timers. Note that after deleting a spawned timer, + the corresponding pattern timer may respawn it. +- Recording is done according to the event's begin/end times, either + by adding the start/stop margins (for non-VPS timers) or by using the + event's running status (for VPS timers). +- The recording of a pattern timer is stored under the given file name, just like + regular timers do. In addition to the "TITLE" and "EPISODE" macros the file + name of a pattern timer can also use "{<}" and "{>}" to reference the part of the + event's title before and after the pattern, respectively. For instance, + if the event's title is "Abc def ghi" and the pattern is "def ", "{<}" + would contain "Abc " and "{>}" would contain "ghi" (note the matching of the + blanks). For completeness, "{=}" can be used to reference the matching + pattern itself. +- In the "Timers" menu pressing the Red button on a pattern timer only toggles the + timer between "on" and "off", even if this is a repeating timer. +- In the "Timers" menu pattern timers are sorted alphabetically to the end of the + list of timers. +- A regular timer that is currently recording can't be changed into a pattern timer. +- In the "Edit timer" menu the file name and pattern are displayed as + separate items. The Yellow button can be used to toggle between a regular + timer and a pattern timer. When going from regular to pattern, the Pattern item will + be initialized with the base part of the file name. +- The characters '^' and '$' can be used at the very beginning and end of + the pattern to anchor the pattern to the begin or end of the title. + Using both of these will match only titles that consist of exactly the given pattern, + with nothing before and nothing after it. +- The Pattern field in the "Edit timer" menu allows blanks at the end of the string, + which may help to separate the text after the matching pattern. +- If the first character of the pattern is '@', an event that matches the + rest of the pattern is only recorded if the resulting recording's file + name (without any folders) is not contained in the donerecs.data file. + This avoids duplicate recordings of the same programme. Timers spawned from + such a pattern timer are marked with the flag tfAvoid. +- When editing the "File" field of a timer, the Blue button can be pressed to + insert one of the macros "TITLE", "EPISODE", "{<}", "{=}" or "{>}", + respectively. Pressing the Blue button repeatedly loops through the available + macros. The "{...}" macros are only available for pattern timers. +- In the "Schedule" and "What's on...?" menus the events that will be recorded + by a pattern timer are marked in the same way as regular timers. +- The TIMERS column in the LCARS skin doesn't show the basic definitions of + pattern timers, it only shows timers actually spawned from pattern timers. + +If the pattern is prepended with '@', the name of the resulting recording (everything +after the rightmost '~', or the entire file name, if there is no '~') will be stored +in the file donerecs.data, so that multiple recordings of the same programme can be +avoided. When using this feature, special care must be taken regarding the recording's +file name. For instance, with a combination of + + pattern file name + @Columbo Movies~TITLE + +if the event's title is just "Columbo", this pattern timer would only record once, +and ignore any future events with that title, even if the episode would be different. +So you may want to use the episode name, as in + + pattern file name + @Columbo Movies~TITLE - EPISODE + +to make the file name unique. If you have several pattern timers for the same show on +different channels, chances are that the broadcasters handle title and episode +differently, as for example in + + TITLE EPISODE pattern file name + Columbo Blueprint for Murder @^Columbo$ TITLE - EPISODE + Columbo - Blueprint for Murder @^Columbo TITLE + Columbo: Blueprint for Murder @^Columbo:_ Columbo - {>} + +(note the '_' in the pattern of the third example; this is just used to visualize +the blank at the end of the pattern) + +In order to have the same episode result in the same recording file name on all +channels, the file name needs to be generated differently for each channel. First +you need to decide on a proper combination of title and episode name, preferably +one that is already used by one of your channels (let's say the second one). +In the first case, title and episode name are correctly put in their respective +places, and "TITLE - EPISODE" as file name will do. The second case is our common +version, where everything is in the title, so TITLE is just fine. The third case +poses a problem, because everything is in the title, but with a different separator. +Here the special macro "{>}" can be used in the file name, which contains everything +following the matching pattern. There are three macros that can be used here: + + {<} everything before the matching pattern + {>} everything after the matching pattern + {=} the matching pattern itself (just for completeness) + * Managing folders The "Select folder" menu, which can be accessed by pressing the "Red" key in diff --git a/config.h b/config.h index 6706f07a..b0a6c015 100644 --- a/config.h +++ b/config.h @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: config.h 4.21 2020/12/22 17:23:51 kls Exp $ + * $Id: config.h 5.1 2020/12/26 15:49:01 kls Exp $ */ #ifndef __CONFIG_H @@ -22,13 +22,13 @@ // VDR's own version number: -#define VDRVERSION "2.4.6" -#define VDRVERSNUM 20406 // Version * 10000 + Major * 100 + Minor +#define VDRVERSION "2.5.1" +#define VDRVERSNUM 20501 // Version * 10000 + Major * 100 + Minor // The plugin API's version number: -#define APIVERSION "2.4.6" -#define APIVERSNUM 20406 // Version * 10000 + Major * 100 + Minor +#define APIVERSION "2.5.1" +#define APIVERSNUM 20501 // Version * 10000 + Major * 100 + Minor // When loading plugins, VDR searches them by their APIVERSION, which // may be smaller than VDRVERSION in case there have been no changes to @@ -46,6 +46,13 @@ #define TIMERMACRO_TITLE "TITLE" #define TIMERMACRO_EPISODE "EPISODE" +#define TIMERMACRO_BEFORE "{<}" +#define TIMERMACRO_MATCH "{=}" +#define TIMERMACRO_AFTER "{>}" + +#define TIMERPATTERN_AVOID "@" +#define TIMERPATTERN_BEGIN "^" +#define TIMERPATTERN_END "$" #define MINOSDWIDTH 480 #define MAXOSDWIDTH 1920 diff --git a/menu.c b/menu.c index 724c0a22..5c6e4832 100644 --- a/menu.c +++ b/menu.c @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: menu.c 4.88 2020/12/12 22:01:01 kls Exp $ + * $Id: menu.c 5.1 2020/12/26 15:49:01 kls Exp $ */ #include "menu.h" @@ -993,6 +993,23 @@ eOSState cMenuFolder::ProcessKey(eKeys Key) // --- cMenuEditTimer -------------------------------------------------------- +static const char *TimerFileMacrosForPattern[] = { + TIMERMACRO_TITLE, + TIMERMACRO_EPISODE, + TIMERMACRO_BEFORE, + TIMERMACRO_MATCH, + TIMERMACRO_AFTER, + "", + NULL + }; + +static const char *TimerFileMacros[] = { + TIMERMACRO_TITLE, + TIMERMACRO_EPISODE, + "", + NULL + }; + const cTimer *cMenuEditTimer::addedTimer = NULL; cMenuEditTimer::cMenuEditTimer(cTimer *Timer, bool New) @@ -1000,6 +1017,7 @@ cMenuEditTimer::cMenuEditTimer(cTimer *Timer, bool New) { SetMenuCategory(mcTimerEdit); addedTimer = NULL; + pattern = NULL; file = NULL; day = firstday = NULL; timer = Timer; @@ -1019,6 +1037,7 @@ cMenuEditTimer::cMenuEditTimer(cTimer *Timer, bool New) Add(new cMenuEditIntItem( tr("Lifetime"), &data.lifetime, 0, MAXLIFETIME)); Add(file = new cMenuEditStrItem( tr("File"), data.file, sizeof(data.file))); SetFirstDayItem(); + SetPatternItem(true); if (data.remote) strn0cpy(remote, data.remote, sizeof(remote)); else @@ -1047,7 +1066,7 @@ const cTimer *cMenuEditTimer::AddedTimer(void) void cMenuEditTimer::SetHelpKeys(void) { - SetHelp(tr("Button$Folder"), data.weekdays ? tr("Button$Single") : tr("Button$Repeating")); + SetHelp(tr("Button$Folder"), data.weekdays ? tr("Button$Single") : tr("Button$Repeating"), *data.pattern ? tr("Button$Regular") : tr("Button$Pattern")); } void cMenuEditTimer::SetFirstDayItem(void) @@ -1063,6 +1082,40 @@ void cMenuEditTimer::SetFirstDayItem(void) } } +void cMenuEditTimer::SetPatternItem(bool Initial) +{ + if (Initial && !*data.pattern) { + file->SetMacros(TimerFileMacros); + return; + } + if (!pattern) { + if (data.HasFlags(tfRecording)) { + Skins.Message(mtWarning, tr("Timer is recording!")); + return; + } + if (!*data.pattern) { + char *p = strrchr(data.file, FOLDERDELIMCHAR); + if (p) + p++; + else + p = data.file; + strn0cpy(data.pattern, p, sizeof(data.pattern)); + } + Ins(pattern = new cMenuEditStrItem( tr("Pattern"), data.pattern, sizeof(data.pattern)), true, file); + pattern->SetKeepSpace(); + file->SetMacros(TimerFileMacrosForPattern); + Display(); + } + else { + Del(pattern->Index()); + pattern = NULL; + *data.pattern = 0; + file->SetMacros(TimerFileMacros); + Display(); + } + SetHelpKeys(); +} + eOSState cMenuEditTimer::SetFolder(void) { if (cMenuFolder *mf = dynamic_cast(SubMenu())) { @@ -1142,6 +1195,8 @@ eOSState cMenuEditTimer::ProcessKey(eKeys Key) cRecordControls::Stop(timer); if (timer->Remote() && data.Remote()) Timers->SetSyncStateKey(StateKeySVDRPRemoteTimersPoll); + if (data.Local() && !timer->IsPatternTimer() && data.IsPatternTimer()) + data.SetEvent(NULL); *timer = data; } LOCK_SCHEDULES_READ; @@ -1159,7 +1214,8 @@ eOSState cMenuEditTimer::ProcessKey(eKeys Key) Display(); } return osContinue; - case kYellow: + case kYellow: SetPatternItem(); + return osContinue; case kBlue: return osContinue; default: break; } @@ -1212,12 +1268,19 @@ void cMenuTimerItem::Set(void) strftime(buffer, sizeof(buffer), "%Y%m%d", &tm_r); day = buffer; } - const char *File = Setup.FoldersInTimerMenu ? NULL : strrchr(timer->File(), FOLDERDELIMCHAR); - if (File && strcmp(File + 1, TIMERMACRO_TITLE) && strcmp(File + 1, TIMERMACRO_EPISODE)) - File++; - else - File = timer->File(); - SetText(cString::sprintf("%c\t%d\t%s%s%s\t%02d:%02d\t%02d:%02d\t%s%s", + const char *File = timer->Pattern(); + if (!*File) { + if (timer->HasFlags(tfSpawned) && timer->Event() && timer->Event()->Title()) + File = timer->Event()->Title(); + else { + File = Setup.FoldersInTimerMenu ? NULL : strrchr(timer->File(), FOLDERDELIMCHAR); + if (File && strcmp(File + 1, TIMERMACRO_TITLE) && strcmp(File + 1, TIMERMACRO_EPISODE)) + File++; + else + File = timer->File(); + } + } + SetText(cString::sprintf("%c\t%d\t%s%s%s\t%02d:%02d\t%02d:%02d\t%s%s%s%s", !(timer->HasFlags(tfActive)) ? ' ' : timer->FirstDay() ? '!' : timer->Recording() ? '#' : '>', timer->Channel()->Number(), *name, @@ -1228,7 +1291,9 @@ void cMenuTimerItem::Set(void) timer->Stop() / 100, timer->Stop() % 100, timer->Remote() ? *cString::sprintf("@%s: ", timer->Remote()) : "", - File)); + timer->IsPatternTimer() ? "{" : "", + File, + timer->IsPatternTimer() ? "}" : "")); } void cMenuTimerItem::SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable) @@ -1544,6 +1609,8 @@ bool cMenuScheduleItem::Update(const cTimers *Timers, bool Force) eTimerMatch OldTimerMatch = timerMatch; bool OldTimerActive = timerActive; const cTimer *Timer = Timers->GetMatch(event, &timerMatch); + if (event->EndTime() < time(NULL) && !event->IsRunning()) + timerMatch = tmNone; timerActive = Timer && Timer->HasFlags(tfActive); if (Force || timerMatch != OldTimerMatch || timerActive != OldTimerActive) { cString buffer; @@ -5354,8 +5421,13 @@ void cRecordControl::Stop(bool ExecuteUserCommand) bool cRecordControl::Process(time_t t) { if (!recorder || !recorder->IsAttached() || !timer || !timer->Matches(t)) { - if (timer) + if (timer) { timer->SetPending(false); + if (timer->HasFlags(tfAvoid)) { + const char *p = strgetlast(timer->File(), FOLDERDELIMCHAR); + DoneRecordingsPattern.Append(p); + } + } return false; } return true; diff --git a/menu.h b/menu.h index 3fc9f749..a192d140 100644 --- a/menu.h +++ b/menu.h @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: menu.h 4.8 2018/04/14 10:24:41 kls Exp $ + * $Id: menu.h 5.1 2020/12/26 15:49:01 kls Exp $ */ #ifndef __MENU_H @@ -79,11 +79,13 @@ private: bool addIfConfirmed; cStringList svdrpServerNames; char remote[HOST_NAME_MAX]; + cMenuEditStrItem *pattern; cMenuEditStrItem *file; cMenuEditDateItem *day; cMenuEditDateItem *firstday; eOSState SetFolder(void); void SetFirstDayItem(void); + void SetPatternItem(bool Initial = false); void SetHelpKeys(void); public: cMenuEditTimer(cTimer *Timer, bool New = false); diff --git a/menuitems.c b/menuitems.c index d764c58c..8c516006 100644 --- a/menuitems.c +++ b/menuitems.c @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: menuitems.c 4.3 2018/03/23 15:37:02 kls Exp $ + * $Id: menuitems.c 5.1 2020/12/26 15:49:01 kls Exp $ */ #include "menuitems.h" @@ -390,6 +390,10 @@ cMenuEditStrItem::cMenuEditStrItem(const char *Name, char *Value, int Length, co allowed = Allowed ? Allowed : tr(FileNameChars); pos = -1; offset = 0; + keepSpace = false; + macro = -1; + lastMacro = -1; + macros = NULL; insert = uppercase = false; newchar = true; lengthUtf8 = 0; @@ -408,6 +412,13 @@ cMenuEditStrItem::~cMenuEditStrItem() delete[] charMapUtf8; } +void cMenuEditStrItem::SetMacros(const char **Macros) +{ + macros = Macros; + macro = 0; + lastMacro = -1; +} + void cMenuEditStrItem::EnterEditMode(void) { if (!valueUtf8) { @@ -430,7 +441,8 @@ void cMenuEditStrItem::LeaveEditMode(bool SaveValue) if (valueUtf8) { if (SaveValue) { Utf8FromArray(valueUtf8, value, length); - stripspace(value); + if (!keepSpace) + stripspace(value); } lengthUtf8 = 0; delete[] valueUtf8; @@ -448,7 +460,7 @@ void cMenuEditStrItem::LeaveEditMode(bool SaveValue) void cMenuEditStrItem::SetHelpKeys(void) { if (InEditMode()) - SetHelp(tr("Button$ABC/abc"), insert ? tr("Button$Overwrite") : tr("Button$Insert"), tr("Button$Delete")); + SetHelp(tr("Button$ABC/abc"), insert ? tr("Button$Overwrite") : tr("Button$Insert"), tr("Button$Delete"), macros ? tr("Button$Macro") : NULL); else SetHelp(NULL); } @@ -581,11 +593,39 @@ void cMenuEditStrItem::Delete(void) lengthUtf8--; } +void cMenuEditStrItem::InsertMacro(void) +{ + if (!macros) + return; + if (lastMacro >= 0) { + int l = strlen(macros[lastMacro]); + while (l-- > 0) + Delete(); + } + const char *p = macros[macro]; + int oldPos = pos; + bool oldInsert = insert; + insert = true; + newchar = true; + while (*p) { + Type(*p); + p++; + } + insert = oldInsert; + pos = oldPos; + lastMacro = macro; + if (!macros[++macro]) + macro = 0; +} + eOSState cMenuEditStrItem::ProcessKey(eKeys Key) { bool SameKey = NORMALKEY(Key) == lastKey; - if (Key != kNone) + if (Key != kNone) { lastKey = NORMALKEY(Key); + if (Key != kBlue) + lastMacro = -1; + } else if (!newchar && k0 <= lastKey && lastKey <= k9 && autoAdvanceTimeout.TimedOut()) { AdvancePos(); newchar = true; @@ -635,8 +675,9 @@ eOSState cMenuEditStrItem::ProcessKey(eKeys Key) return osUnknown; break; case kBlue|k_Repeat: - case kBlue: // consume the key only if in edit-mode - if (!InEditMode()) + case kBlue: if (InEditMode()) + InsertMacro(); + else return osUnknown; break; case kLeft|k_Repeat: diff --git a/menuitems.h b/menuitems.h index 44f6fec5..f3e7c1e5 100644 --- a/menuitems.h +++ b/menuitems.h @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: menuitems.h 4.1 2015/09/06 10:38:37 kls Exp $ + * $Id: menuitems.h 5.1 2020/12/26 15:49:01 kls Exp $ */ #ifndef __MENUITEMS_H @@ -111,6 +111,9 @@ private: int length; const char *allowed; int pos, offset; + bool keepSpace; + const char **macros; + int macro, lastMacro; bool insert, newchar, uppercase; int lengthUtf8; uint *valueUtf8; @@ -127,6 +130,7 @@ private: void Type(uint c); void Insert(void); void Delete(void); + void InsertMacro(void); protected: void EnterEditMode(void); void LeaveEditMode(bool SaveValue = false); @@ -134,6 +138,8 @@ protected: public: cMenuEditStrItem(const char *Name, char *Value, int Length, const char *Allowed = NULL); ~cMenuEditStrItem(); + void SetKeepSpace(void) { keepSpace = true; } + void SetMacros(const char **Macros); virtual eOSState ProcessKey(eKeys Key); }; diff --git a/po/ar.po b/po/ar.po index c47e3df6..31f7ce65 100644 --- a/po/ar.po +++ b/po/ar.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2008-10-16 11:16-0400\n" "Last-Translator: Osama Alrawab \n" "Language-Team: Arabic \n" @@ -691,9 +691,21 @@ msgstr "Single" msgid "Button$Repeating" msgstr "Repeating" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "اليوم الاول" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1495,6 +1507,9 @@ msgstr "اعادة الكتابة" msgid "Button$Insert" msgstr "ادراج" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "الملحق" diff --git a/po/ca_ES.po b/po/ca_ES.po index 3c5f55f2..78d77db7 100644 --- a/po/ca_ES.po +++ b/po/ca_ES.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2008-03-02 19:02+0100\n" "Last-Translator: Luca Olivetti \n" "Language-Team: Catalan \n" @@ -690,9 +690,21 @@ msgstr "Individual" msgid "Button$Repeating" msgstr "Repetitiu" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Primer dia" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1494,6 +1506,9 @@ msgstr "Sobrescriure" msgid "Button$Insert" msgstr "Inserir" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Plugin" diff --git a/po/cs_CZ.po b/po/cs_CZ.po index dab1d363..60083811 100644 --- a/po/cs_CZ.po +++ b/po/cs_CZ.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2010-05-06 11:00+0200\n" "Last-Translator: Aleš Juřík \n" "Language-Team: Czech \n" @@ -690,9 +690,21 @@ msgstr "Bez opakování" msgid "Button$Repeating" msgstr "S opakováním" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "První den" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1494,6 +1506,9 @@ msgstr "Přepsat" msgid "Button$Insert" msgstr "Vložit" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Modul" diff --git a/po/da_DK.po b/po/da_DK.po index e434d16c..632eaf90 100644 --- a/po/da_DK.po +++ b/po/da_DK.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2007-08-12 14:17+0200\n" "Last-Translator: Mogens Elneff \n" "Language-Team: Danish \n" @@ -687,9 +687,21 @@ msgstr "" msgid "Button$Repeating" msgstr "" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Frste dag" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1491,6 +1503,9 @@ msgstr "Overskriv" msgid "Button$Insert" msgstr "Indst" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Plugin" diff --git a/po/de_DE.po b/po/de_DE.po index 97fd3b8d..2253d347 100644 --- a/po/de_DE.po +++ b/po/de_DE.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2015-02-10 13:45+0100\n" "Last-Translator: Klaus Schmidinger \n" "Language-Team: German \n" @@ -688,9 +688,21 @@ msgstr "Einmalig" msgid "Button$Repeating" msgstr "Wiederholend" +msgid "Button$Regular" +msgstr "Normal" + +msgid "Button$Pattern" +msgstr "Muster" + msgid "First day" msgstr "Erster Tag" +msgid "Timer is recording!" +msgstr "Timer nimmt auf!" + +msgid "Pattern" +msgstr "Muster" + msgid "Error while accessing remote timer" msgstr "Fehler beim Ansprechen des fernen Timers" @@ -1492,6 +1504,9 @@ msgstr " msgid "Button$Insert" msgstr "Einfgen" +msgid "Button$Macro" +msgstr "Makro" + msgid "Plugin" msgstr "Plugin" diff --git a/po/el_GR.po b/po/el_GR.po index 2642f967..f2fbd181 100644 --- a/po/el_GR.po +++ b/po/el_GR.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2007-08-12 14:17+0200\n" "Last-Translator: Dimitrios Dimitrakos \n" "Language-Team: Greek \n" @@ -687,9 +687,21 @@ msgstr "" msgid "Button$Repeating" msgstr "" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr " " +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1491,6 +1503,9 @@ msgstr " msgid "Button$Insert" msgstr "" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "" diff --git a/po/es_ES.po b/po/es_ES.po index fff14088..f833c499 100644 --- a/po/es_ES.po +++ b/po/es_ES.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2015-02-19 23:00+0100\n" "Last-Translator: Gabriel Bonich \n" "Language-Team: Spanish \n" @@ -688,9 +688,21 @@ msgstr "Individual" msgid "Button$Repeating" msgstr "Peridico" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Primer da" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1492,6 +1504,9 @@ msgstr "Sobreescribir" msgid "Button$Insert" msgstr "Insertar" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Plugin" diff --git a/po/et_EE.po b/po/et_EE.po index e570a449..31601b1c 100644 --- a/po/et_EE.po +++ b/po/et_EE.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2007-08-12 14:17+0200\n" "Last-Translator: Arthur Konovalov \n" "Language-Team: Estonian \n" @@ -687,9 +687,21 @@ msgstr "Üksik" msgid "Button$Repeating" msgstr "Korduv" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "1. päev" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "Kaugtaimeri viga" @@ -1491,6 +1503,9 @@ msgstr "Asenda (OVR)" msgid "Button$Insert" msgstr "Lisa (INS)" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Plugin" diff --git a/po/fi_FI.po b/po/fi_FI.po index 310caa6f..ffae4685 100644 --- a/po/fi_FI.po +++ b/po/fi_FI.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2007-08-15 15:52+0200\n" "Last-Translator: Matti Lehtimäki \n" "Language-Team: Finnish \n" @@ -691,9 +691,21 @@ msgstr "Yksittäinen" msgid "Button$Repeating" msgstr "Toistuva" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "1. päivä" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "Etäajastimen hakeminen epäonnistui" @@ -1495,6 +1507,9 @@ msgstr "Korvaa" msgid "Button$Insert" msgstr "Lisää" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Laajennos" diff --git a/po/fr_FR.po b/po/fr_FR.po index 51b2fa7f..f6613822 100644 --- a/po/fr_FR.po +++ b/po/fr_FR.po @@ -18,7 +18,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2018-04-14 10:16+0100\n" "Last-Translator: Bernard Jaulin \n" "Language-Team: French \n" @@ -698,9 +698,21 @@ msgstr "Simple" msgid "Button$Repeating" msgstr "Périodique" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Premier jour" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "Erreur pendant l'accès à la programmation" @@ -1502,6 +1514,9 @@ msgstr "Écraser" msgid "Button$Insert" msgstr "Insérer" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Module" diff --git a/po/hr_HR.po b/po/hr_HR.po index 60bf92df..b98a19c8 100644 --- a/po/hr_HR.po +++ b/po/hr_HR.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2008-03-17 19:00+0100\n" "Last-Translator: Adrian Caval \n" "Language-Team: Croatian \n" @@ -689,9 +689,21 @@ msgstr "" msgid "Button$Repeating" msgstr "" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Prvi dan" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1493,6 +1505,9 @@ msgstr "Prepi msgid "Button$Insert" msgstr "Umetni" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Dodatak" diff --git a/po/hu_HU.po b/po/hu_HU.po index 3f9b249a..c05543c9 100644 --- a/po/hu_HU.po +++ b/po/hu_HU.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2018-04-09 21:42+0300\n" "Last-Translator: István Füley \n" "Language-Team: Hungarian \n" @@ -692,9 +692,21 @@ msgstr "Egyszeri" msgid "Button$Repeating" msgstr "Ismétlődő" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Első nap" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "Távoli időzítő nem elérhető" @@ -1496,6 +1508,9 @@ msgstr "Felülírás" msgid "Button$Insert" msgstr "Beillesztés" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Plugin" diff --git a/po/it_IT.po b/po/it_IT.po index a07c33de..168d8a63 100644 --- a/po/it_IT.po +++ b/po/it_IT.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2018-04-06 19:13+0100\n" "Last-Translator: Gringo \n" "Language-Team: Italian \n" @@ -693,9 +693,21 @@ msgstr "Una volta" msgid "Button$Repeating" msgstr "Repliche" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "1° giorno" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "Errore durante l'accesso al timer remoto" @@ -1497,6 +1509,9 @@ msgstr "Sovrascrivi" msgid "Button$Insert" msgstr "Inserisci" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Plugin" diff --git a/po/lt_LT.po b/po/lt_LT.po index 23b47d3a..53a50315 100644 --- a/po/lt_LT.po +++ b/po/lt_LT.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2015-02-11 14:02+0200\n" "Last-Translator: Valdemaras Pipiras \n" "Language-Team: Lithuanian \n" @@ -687,9 +687,21 @@ msgstr "Vienas" msgid "Button$Repeating" msgstr "Kartotinas" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Pirma diena" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1491,6 +1503,9 @@ msgstr "Perrąšyti" msgid "Button$Insert" msgstr "Įterpti" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Įskiepas" diff --git a/po/mk_MK.po b/po/mk_MK.po index cc892c11..3c94ae6f 100644 --- a/po/mk_MK.po +++ b/po/mk_MK.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2018-03-31 21:47+0100\n" "Last-Translator: Dimitar Petrovski \n" "Language-Team: Macedonian \n" @@ -689,9 +689,21 @@ msgstr "Единчен" msgid "Button$Repeating" msgstr "Периодичен" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Прв ден" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "Грешка при пристап на далечен тајмер" @@ -1493,6 +1505,9 @@ msgstr "Препиши" msgid "Button$Insert" msgstr "Вметни" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Додаток" diff --git a/po/nl_NL.po b/po/nl_NL.po index f14533ff..b1154fdb 100644 --- a/po/nl_NL.po +++ b/po/nl_NL.po @@ -13,7 +13,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2015-02-10 19:43+0100\n" "Last-Translator: Erik Oomen \n" "Language-Team: Dutch \n" @@ -693,9 +693,21 @@ msgstr "Eenmalig" msgid "Button$Repeating" msgstr "Herhalen" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Eerste dag" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1497,6 +1509,9 @@ msgstr "Overschrijven" msgid "Button$Insert" msgstr "Invoegen" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Plugin" diff --git a/po/nn_NO.po b/po/nn_NO.po index a222d747..15822052 100644 --- a/po/nn_NO.po +++ b/po/nn_NO.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2007-08-12 14:17+0200\n" "Last-Translator: Truls Slevigen \n" "Language-Team: Norwegian Nynorsk \n" @@ -688,9 +688,21 @@ msgstr "" msgid "Button$Repeating" msgstr "" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Frste dag" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1492,6 +1504,9 @@ msgstr "" msgid "Button$Insert" msgstr "" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Plugin" diff --git a/po/pl_PL.po b/po/pl_PL.po index 7b81b858..9c71c1ba 100644 --- a/po/pl_PL.po +++ b/po/pl_PL.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2018-02-19 00:42+0100\n" "Last-Translator: Tomasz Maciej Nowak \n" "Language-Team: Polish \n" @@ -692,9 +692,21 @@ msgstr "Pojedynczy" msgid "Button$Repeating" msgstr "Powtarzanie" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Pierwszy dzień" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "Błąd podczas dostępu do zdalnego timera" @@ -1496,6 +1508,9 @@ msgstr "Nadpisz" msgid "Button$Insert" msgstr "Wstaw" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Wtyczka" diff --git a/po/pt_PT.po b/po/pt_PT.po index b0fd0676..49034596 100644 --- a/po/pt_PT.po +++ b/po/pt_PT.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2010-03-28 22:49+0100\n" "Last-Translator: Cris Silva \n" "Language-Team: Portuguese \n" @@ -688,9 +688,21 @@ msgstr "" msgid "Button$Repeating" msgstr "" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "1 dia" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1492,6 +1504,9 @@ msgstr "Sobrescrever" msgid "Button$Insert" msgstr "Inserir" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Plugin" diff --git a/po/ro_RO.po b/po/ro_RO.po index 3e097216..c0aab78d 100644 --- a/po/ro_RO.po +++ b/po/ro_RO.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2015-02-11 22:26+0100\n" "Last-Translator: Lucian Muresan \n" "Language-Team: Romanian \n" @@ -689,9 +689,21 @@ msgstr "Odată" msgid "Button$Repeating" msgstr "Repetitiv" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Prima zi" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1493,6 +1505,9 @@ msgstr "Suprascrie" msgid "Button$Insert" msgstr "Inserează" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Plugin" diff --git a/po/ru_RU.po b/po/ru_RU.po index 76df4a56..f8be4b4c 100644 --- a/po/ru_RU.po +++ b/po/ru_RU.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2016-12-27 17:13+0100\n" "Last-Translator: Pridvorov Andrey \n" "Language-Team: Russian \n" @@ -688,9 +688,21 @@ msgstr "Один раз" msgid "Button$Repeating" msgstr "Повтор" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Первый день" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "Ошибка доступа к таймеру" @@ -1492,6 +1504,9 @@ msgstr "Замена" msgid "Button$Insert" msgstr "Вставка" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Модуль" diff --git a/po/sk_SK.po b/po/sk_SK.po index d425023b..1374a578 100644 --- a/po/sk_SK.po +++ b/po/sk_SK.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2015-02-17 18:59+0100\n" "Last-Translator: Milan Hrala \n" "Language-Team: Slovak \n" @@ -688,9 +688,21 @@ msgstr "bez opakovania" msgid "Button$Repeating" msgstr "s opakovanm" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Odo da" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1492,6 +1504,9 @@ msgstr "Prep msgid "Button$Insert" msgstr "Vloi" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Modul" diff --git a/po/sl_SI.po b/po/sl_SI.po index 73716aab..504f942c 100644 --- a/po/sl_SI.po +++ b/po/sl_SI.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2013-03-04 12:46+0100\n" "Last-Translator: Matjaz Thaler \n" "Language-Team: Slovenian \n" @@ -688,9 +688,21 @@ msgstr "Enkraten" msgid "Button$Repeating" msgstr "Ponavljajoe" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Prvi dan" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1492,6 +1504,9 @@ msgstr "Prepi msgid "Button$Insert" msgstr "Vstavi" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Vstavek" diff --git a/po/sr_RS.po b/po/sr_RS.po index b8a73ca2..4836ca8e 100644 --- a/po/sr_RS.po +++ b/po/sr_RS.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2013-03-16 15:05+0100\n" "Last-Translator: Zoran Turalija \n" "Language-Team: Serbian \n" @@ -688,9 +688,21 @@ msgstr "Jedinstven" msgid "Button$Repeating" msgstr "Ponavljajui" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Prvi dan" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1492,6 +1504,9 @@ msgstr "Zameni" msgid "Button$Insert" msgstr "Ubaci" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Dodatak" diff --git a/po/sv_SE.po b/po/sv_SE.po index d7d82476..432f9fef 100644 --- a/po/sv_SE.po +++ b/po/sv_SE.po @@ -12,7 +12,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2015-02-12 21:58+0100\n" "Last-Translator: Magnus Sirvi \n" "Language-Team: Swedish \n" @@ -692,9 +692,21 @@ msgstr "Enskilld" msgid "Button$Repeating" msgstr "Repeterande" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Frsta dag" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1496,6 +1508,9 @@ msgstr "Skriv msgid "Button$Insert" msgstr "Infoga" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Modul" diff --git a/po/tr_TR.po b/po/tr_TR.po index cf8f1f02..9c7a24d5 100644 --- a/po/tr_TR.po +++ b/po/tr_TR.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2008-02-28 00:33+0100\n" "Last-Translator: Oktay Yolgeen \n" "Language-Team: Turkish \n" @@ -687,9 +687,21 @@ msgstr "" msgid "Button$Repeating" msgstr "" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "lk gn" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1491,6 +1503,9 @@ msgstr " msgid "Button$Insert" msgstr "Ekle" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Eklenti" diff --git a/po/uk_UA.po b/po/uk_UA.po index 73927c7c..63ab92eb 100644 --- a/po/uk_UA.po +++ b/po/uk_UA.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2018-03-18 20:00+0100\n" "Last-Translator: Yarema aka Knedlyk \n" "Language-Team: Ukrainian \n" @@ -688,9 +688,21 @@ msgstr "Одинарне" msgid "Button$Repeating" msgstr "Повтор" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "Перший день" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "Помилка доступу до віддаленого таймера" @@ -1492,6 +1504,9 @@ msgstr "Заміна" msgid "Button$Insert" msgstr "Вставка" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "Модуль" diff --git a/po/zh_CN.po b/po/zh_CN.po index cb300438..715a3b7c 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: VDR 2.4.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-06-15 17:50+0200\n" +"POT-Creation-Date: 2020-12-24 17:42+0100\n" "PO-Revision-Date: 2013-03-04 14:52+0800\n" "Last-Translator: NFVDR \n" "Language-Team: Chinese (simplified) \n" @@ -689,9 +689,21 @@ msgstr "单个" msgid "Button$Repeating" msgstr "重复" +msgid "Button$Regular" +msgstr "" + +msgid "Button$Pattern" +msgstr "" + msgid "First day" msgstr "第一天" +msgid "Timer is recording!" +msgstr "" + +msgid "Pattern" +msgstr "" + msgid "Error while accessing remote timer" msgstr "" @@ -1493,6 +1505,9 @@ msgstr "覆盖" msgid "Button$Insert" msgstr "插入" +msgid "Button$Macro" +msgstr "" + msgid "Plugin" msgstr "插件" diff --git a/recording.c b/recording.c index 6bfbcff1..17467e36 100644 --- a/recording.c +++ b/recording.c @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: recording.c 4.29 2020/10/30 16:08:29 kls Exp $ + * $Id: recording.c 5.1 2020/12/26 15:49:01 kls Exp $ */ #include "recording.h" @@ -3050,6 +3050,74 @@ cUnbufferedFile *cFileName::NextFile(void) return SetOffset(fileNumber + 1); } +// --- cDoneRecordings ------------------------------------------------------- + +cDoneRecordings DoneRecordingsPattern; + +bool cDoneRecordings::Load(const char *FileName) +{ + fileName = FileName; + if (*fileName && access(fileName, F_OK) == 0) { + isyslog("loading %s", *fileName); + FILE *f = fopen(fileName, "r"); + if (f) { + char *s; + cReadLine ReadLine; + while ((s = ReadLine.Read(f)) != NULL) + Add(s); + fclose(f); + } + else { + LOG_ERROR_STR(*fileName); + return false; + } + } + return true; +} + +bool cDoneRecordings::Save(void) const +{ + bool result = true; + cSafeFile f(fileName); + if (f.Open()) { + for (int i = 0; i < doneRecordings.Size(); i++) { + if (fputs(doneRecordings[i], f) == EOF || fputc('\n', f) == EOF) { + result = false; + break; + } + } + if (!f.Close()) + result = false; + } + else + result = false; + return result; +} + +void cDoneRecordings::Add(const char *Title) +{ + doneRecordings.Append(strdup(Title)); +} + +void cDoneRecordings::Append(const char *Title) +{ + if (!Contains(Title)) { + Add(Title); + if (FILE *f = fopen(fileName, "a")) { + fputs(Title, f); + fputc('\n', f); + fclose(f); + } + else + esyslog("ERROR: can't open '%s' for appending '%s'", *fileName, Title); + } +} + +bool cDoneRecordings::Contains(const char *Title) const +{ + return doneRecordings.Find(Title) >= 0; +} + // --- Index stuff ----------------------------------------------------------- cString IndexToHMSF(int Index, bool WithFrame, double FramesPerSecond) diff --git a/recording.h b/recording.h index 1ba16bdc..6f26f2f6 100644 --- a/recording.h +++ b/recording.h @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: recording.h 4.10 2020/09/16 13:48:33 kls Exp $ + * $Id: recording.h 5.1 2020/12/26 15:49:01 kls Exp $ */ #ifndef __RECORDING_H @@ -504,6 +504,20 @@ public: cUnbufferedFile *NextFile(void); }; +class cDoneRecordings { +private: + cString fileName; + cStringList doneRecordings; + void Add(const char *Title); +public: + bool Load(const char *FileName); + bool Save(void) const; + void Append(const char *Title); + bool Contains(const char *Title) const; + }; + +extern cDoneRecordings DoneRecordingsPattern; + cString IndexToHMSF(int Index, bool WithFrame = false, double FramesPerSecond = DEFAULTFRAMESPERSECOND); // Converts the given index to a string, optionally containing the frame number. int HMSFToIndex(const char *HMSF, double FramesPerSecond = DEFAULTFRAMESPERSECOND); diff --git a/skinlcars.c b/skinlcars.c index 0f431631..36d561f7 100644 --- a/skinlcars.c +++ b/skinlcars.c @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: skinlcars.c 4.7 2020/05/18 16:47:29 kls Exp $ + * $Id: skinlcars.c 5.1 2020/12/26 15:49:01 kls Exp $ */ // "Star Trek: The Next Generation"(R) is a registered trademark of Paramount Pictures, @@ -1264,7 +1264,9 @@ void cSkinLCARSDisplayMenu::DrawTimers(void) if (y + lineHeight > ys05) break; if (const cTimer *Timer = SortedTimers[i]) { - if (Timer->Recording()) { + if (Timer->IsPatternTimer()) + SortedTimers[i] = NULL; + else if (Timer->Recording()) { if (Timer->Remote()) { if (!Device && Timer->HasFlags(tfActive)) { DrawTimer(Timer, y, false); diff --git a/svdrp.c b/svdrp.c index 38bd8bb1..8adf0e0c 100644 --- a/svdrp.c +++ b/svdrp.c @@ -10,7 +10,7 @@ * and interact with the Video Disk Recorder - or write a full featured * graphical interface that sits on top of an SVDRP connection. * - * $Id: svdrp.c 4.43 2020/06/22 20:59:49 kls Exp $ + * $Id: svdrp.c 5.1 2020/12/26 15:49:01 kls Exp $ */ #include "svdrp.h" @@ -2048,6 +2048,10 @@ void cSVDRPServer::CmdMODT(const char *Option) Reply(501, "Error in timer settings"); return; } + if (IsRecording && t.IsPatternTimer()) { + Reply(550, "Timer is recording"); + return; + } *Timer = t; if (IsRecording) Timer->SetFlags(tfRecording); @@ -2055,6 +2059,8 @@ void cSVDRPServer::CmdMODT(const char *Option) Timer->ClrFlags(tfRecording); Timers->SetModified(); isyslog("SVDRP %s < %s modified timer %s (%s)", Setup.SVDRPHostName, *clientName, *Timer->ToDescr(), Timer->HasFlags(tfActive) ? "active" : "inactive"); + if (Timer->IsPatternTimer()) + Timer->SetEvent(NULL); Reply(250, "%d %s", Timer->Id(), *Timer->ToText(true)); } else diff --git a/timers.c b/timers.c index 874cfad1..2f16c8c8 100644 --- a/timers.c +++ b/timers.c @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: timers.c 4.20 2020/09/16 13:48:33 kls Exp $ + * $Id: timers.c 5.1 2020/12/26 15:49:01 kls Exp $ */ #include "timers.h" @@ -31,6 +31,7 @@ cTimer::cTimer(bool Instant, bool Pause, const cChannel *Channel) deferred = 0; pending = inVpsMargin = false; flags = tfNone; + *pattern = 0; *file = 0; aux = NULL; remote = NULL; @@ -81,7 +82,94 @@ cTimer::cTimer(bool Instant, bool Pause, const cChannel *Channel) snprintf(file, sizeof(file), "%s%s", Setup.MarkInstantRecord ? "@" : "", *Setup.NameInstantRecord ? Setup.NameInstantRecord : channel->Name()); } -cTimer::cTimer(const cEvent *Event) +static bool MatchPattern(const char *Pattern, const char *Title, cString *Before = NULL, cString *Match = NULL, cString *After = NULL) +{ + if (Title) { + bool AvoidDuplicates = startswith(Pattern, TIMERPATTERN_AVOID); + if (AvoidDuplicates) + Pattern++; + if (strcmp(Pattern, "*") == 0) { + if (Before) + *Before = ""; + if (Match) + *Match = Title; + if (After) + *After = ""; + return true; + } + bool AnchorBegin = startswith(Pattern, TIMERPATTERN_BEGIN); + if (AnchorBegin) + Pattern++; + bool AnchorEnd = endswith(Pattern, TIMERPATTERN_END); + cNullTerminate nt; + if (AnchorEnd) + nt.Set(const_cast(Pattern + strlen(Pattern) - 1)); + if (AnchorBegin && AnchorEnd) { + if (strcmp(Title, Pattern) == 0) { + if (Before) + *Before = ""; + if (Match) + *Match = Title; + if (After) + *After = ""; + return true; + } + } + else if (AnchorBegin) { + if (strstr(Title, Pattern) == Title) { + if (Before) + *Before = ""; + if (Match) + *Match = Pattern; + if (After) + *After = cString(Title + strlen(Pattern)); + return true; + } + } + else if (AnchorEnd) { + if (endswith(Title, Pattern)) { + if (Before) + *Before = cString(Title, Title + strlen(Title) - strlen(Pattern)); + if (Match) + *Match = Pattern; + if (After) + *After = ""; + return true; + } + } + else if (const char *p = strstr(Title, Pattern)) { + if (Before) + *Before = cString(Title, p); + if (Match) + *Match = Pattern; + if (After) + *After = cString(p + strlen(Pattern)); + return true; + } + } + return false; +} + +static cString MakePatternFileName(const char *Pattern, const char *Title, const char *Episode, const char *File) +{ + if (!Pattern || !Title || !File) + return NULL; + cString Before = ""; + cString Match = ""; + cString After = ""; + if (MatchPattern(Pattern, Title, &Before, &Match, &After)) { + char *Result = strdup(File); + Result = strreplace(Result, TIMERMACRO_TITLE, Title); + Result = strreplace(Result, TIMERMACRO_EPISODE, Episode); + Result = strreplace(Result, TIMERMACRO_BEFORE, Before); + Result = strreplace(Result, TIMERMACRO_MATCH, Match); + Result = strreplace(Result, TIMERMACRO_AFTER, After); + return cString(Result, true);; + } + return NULL; +} + +cTimer::cTimer(const cEvent *Event, const char *FileName, const cTimer *PatternTimer) { id = 0; startTime = stopTime = 0; @@ -89,12 +177,15 @@ cTimer::cTimer(const cEvent *Event) deferred = 0; pending = inVpsMargin = false; flags = tfActive; + *pattern = 0; *file = 0; aux = NULL; remote = NULL; event = NULL; - if (Event->Vps() && Setup.UseVps) - SetFlags(tfVps); + if (!PatternTimer || PatternTimer->HasFlags(tfVps)) { + if (Event->Vps() && Setup.UseVps) + SetFlags(tfVps); + } LOCK_CHANNELS_READ; channel = Channels->GetByChannelID(Event->ChannelID(), true); time_t tstart = (flags & tfVps) ? Event->Vps() : Event->StartTime(); @@ -112,11 +203,12 @@ cTimer::cTimer(const cEvent *Event) stop = time->tm_hour * 100 + time->tm_min; if (stop >= 2400) stop -= 2400; - priority = Setup.DefaultPriority; - lifetime = Setup.DefaultLifetime; - const char *Title = Event->Title(); - if (!isempty(Title)) - Utf8Strn0Cpy(file, Event->Title(), sizeof(file)); + priority = PatternTimer ? PatternTimer->Priority() : Setup.DefaultPriority; + lifetime = PatternTimer ? PatternTimer->Lifetime() : Setup.DefaultLifetime; + if (!FileName) + FileName = Event->Title(); + if (!isempty(FileName)) + Utf8Strn0Cpy(file, FileName, sizeof(file)); SetEvent(Event); } @@ -156,6 +248,7 @@ cTimer& cTimer::operator= (const cTimer &Timer) stop = Timer.stop; priority = Timer.priority; lifetime = Timer.lifetime; + strncpy(pattern, Timer.pattern, sizeof(pattern)); strncpy(file, Timer.file, sizeof(file)); free(aux); aux = Timer.aux ? strdup(Timer.aux) : NULL; @@ -178,20 +271,37 @@ int cTimer::Compare(const cListObject &ListObject) const int r = t1 - t2; if (r == 0) r = ti->priority - priority; + if (IsPatternTimer() ^ ti->IsPatternTimer()) { + if (IsPatternTimer()) + r = 1; + else + r = -1; + } + else if (IsPatternTimer() && ti->IsPatternTimer()) + r = strcoll(Pattern(), ti->Pattern()); return r; } +cString cTimer::PatternAndFile(void) const +{ + if (IsPatternTimer()) + return cString::sprintf("{%s}%s", pattern, file); + return file; +} + cString cTimer::ToText(bool UseChannelID) const { + strreplace(pattern, ':', '|'); strreplace(file, ':', '|'); - cString buffer = cString::sprintf("%u:%s:%s:%04d:%04d:%d:%d:%s:%s", flags, UseChannelID ? *Channel()->GetChannelID().ToString() : *itoa(Channel()->Number()), *PrintDay(day, weekdays, true), start, stop, priority, lifetime, file, aux ? aux : ""); + cString buffer = cString::sprintf("%u:%s:%s:%04d:%04d:%d:%d:%s:%s", flags, UseChannelID ? *Channel()->GetChannelID().ToString() : *itoa(Channel()->Number()), *PrintDay(day, weekdays, true), start, stop, priority, lifetime, *PatternAndFile(), aux ? aux : ""); + strreplace(pattern, '|', ':'); strreplace(file, '|', ':'); return buffer; } cString cTimer::ToDescr(void) const { - return cString::sprintf("%d%s%s (%d %04d-%04d %s'%s')", Id(), remote ? "@" : "", remote ? remote : "", Channel()->Number(), start, stop, HasFlags(tfVps) ? "VPS " : "", file); + return cString::sprintf("%d%s%s (%d %04d-%04d %s'%s')", Id(), remote ? "@" : "", remote ? remote : "", Channel()->Number(), start, stop, HasFlags(tfVps) ? "VPS " : "", *PatternAndFile()); } int cTimer::TimeToInt(int t) @@ -332,7 +442,18 @@ bool cTimer::Parse(const char *s) } //TODO add more plausibility checks result = ParseDay(daybuffer, day, weekdays); - Utf8Strn0Cpy(file, filebuffer, sizeof(file)); + char *fb = filebuffer; + if (*fb == '{') { + if (char *p = strchr(fb, '}')) { + *p = 0; + Utf8Strn0Cpy(pattern, fb + 1, sizeof(pattern)); + strreplace(pattern, '|', ':'); + fb = p + 1; + } + } + else + *pattern = 0; + Utf8Strn0Cpy(file, fb, sizeof(file)); strreplace(file, '|', ':'); LOCK_CHANNELS_READ; if (isnumber(channelbuffer)) @@ -404,6 +525,11 @@ time_t cTimer::SetTime(time_t t, int SecondsFromMidnight) return mktime(&tm); } +void cTimer::SetPattern(const char *Pattern) +{ + Utf8Strn0Cpy(pattern, Pattern, sizeof(pattern)); +} + void cTimer::SetFile(const char *File) { if (!isempty(File)) @@ -451,6 +577,9 @@ bool cTimer::Matches(time_t t, bool Directly, int Margin) const day = 0; } + if (IsPatternTimer()) + return false; // we only need to have start/stopTime initialized + if (t < deferred) return false; deferred = 0; @@ -483,6 +612,21 @@ eTimerMatch cTimer::Matches(const cEvent *Event, int *Overlap) const // gets 200 added to the FULLMATCH. if (channel->GetChannelID() == Event->ChannelID()) { bool UseVps = HasFlags(tfVps) && Event->Vps(); + if (IsPatternTimer()) { + if (startswith(Pattern(), TIMERPATTERN_AVOID)) { + cString FileName = MakePatternFileName(Pattern(), Event->Title(), Event->ShortText(), File()); + if (*FileName) { + const char *p = strgetlast(*FileName, FOLDERDELIMCHAR); + if (DoneRecordingsPattern.Contains(p)) + return tmNone; + } + else + return tmNone; + } + else if (!MatchPattern(Pattern(), Event->Title())) + return tmNone; + UseVps = false; + } Matches(UseVps ? Event->Vps() : Event->StartTime(), true); int overlap = 0; if (UseVps) { @@ -499,8 +643,11 @@ eTimerMatch cTimer::Matches(const cEvent *Event, int *Overlap) const overlap = FULLMATCH; else if (stopTime <= Event->StartTime() || Event->EndTime() <= startTime) overlap = 0; - else + else { overlap = (min(stopTime, Event->EndTime()) - max(startTime, Event->StartTime())) * FULLMATCH / max(Event->Duration(), 1); + if (IsPatternTimer() && overlap > 0) + overlap = FULLMATCH; + } } startTime = stopTime = 0; if (Overlap) @@ -542,8 +689,60 @@ void cTimer::SetId(int Id) id = Id; } +void cTimer::SpawnPatternTimer(const cEvent *Event, cTimers *Timers) +{ + cString FileName = MakePatternFileName(Pattern(), Event->Title(), Event->ShortText(), File()); + isyslog("spawning timer %s for event %s", *ToDescr(), *Event->ToDescr()); + cTimer *t = new cTimer(Event, FileName, this); + t->SetFlags(tfSpawned); + if (startswith(Pattern(), TIMERPATTERN_AVOID)) + t->SetFlags(tfAvoid); + Timers->Add(t); + HandleRemoteTimerModifications(t); +} + +bool cTimer::SpawnPatternTimers(const cSchedules *Schedules, cTimers *Timers) +{ + bool TimersSpawned = false; + const cSchedule *Schedule = Schedules->GetSchedule(Channel()); + if (Schedule && Schedule->Events()->First()) { + if (Schedule->Modified(scheduleState)) { + time_t Now = time(NULL); + for (const cEvent *e = Schedule->Events()->First(); e; e = Schedule->Events()->Next(e)) { + if (Matches(e) != tmNone) { + bool CheckThis = false; + bool CheckNext = false; + if (e->HasTimer()) // a matching event that already has a timer + CheckNext = true; + else if (e->EndTime() > Now) { // only look at events that have not yet ended + CheckThis = true; + CheckNext = true; + } + if (CheckThis) { + SpawnPatternTimer(e, Timers); + TimersSpawned = true; + } + if (CheckNext) { + // We also check the event immediately following this one: + e = Schedule->Events()->Next(e); + if (e && !e->HasTimer() && Matches(e) != tmNone) { + SpawnPatternTimer(e, Timers); + TimersSpawned = true; + } + } + if (CheckThis || CheckNext) + break; + } + } + } + } + return TimersSpawned; +} + bool cTimer::SetEventFromSchedule(const cSchedules *Schedules) { + if (IsPatternTimer()) + return SetEvent(NULL); const cSchedule *Schedule = Schedules->GetSchedule(Channel()); if (Schedule && Schedule->Events()->First()) { if (Schedule->Modified(scheduleState)) { @@ -707,7 +906,7 @@ void cTimer::Skip(void) void cTimer::OnOff(void) { - if (IsSingleEvent()) + if (IsSingleEvent() || IsPatternTimer()) InvFlags(tfActive); else if (day) { day = 0; @@ -718,6 +917,8 @@ void cTimer::OnOff(void) else SetFlags(tfActive); SetEvent(NULL); + if (HasFlags(tfActive)) + scheduleState = -1; // have pattern timers spawn if necessary Matches(); // refresh start and end time } @@ -831,7 +1032,7 @@ const cTimer *cTimers::GetNextActiveTimer(void) const { const cTimer *t0 = NULL; for (const cTimer *ti = First(); ti; ti = Next(ti)) { - if (!ti->Remote()) { + if (!ti->Remote() && !ti->IsPatternTimer()) { ti->Matches(); if ((ti->HasFlags(tfActive)) && (!t0 || ti->StopTime() > time(NULL) && ti->Compare(*t0) < 0)) t0 = ti; @@ -882,8 +1083,22 @@ const cTimer *cTimers::UsesChannel(const cChannel *Channel) const bool cTimers::SetEvents(const cSchedules *Schedules) { bool TimersModified = false; - for (cTimer *ti = First(); ti; ti = Next(ti)) - TimersModified |= ti->SetEventFromSchedule(Schedules); + for (cTimer *ti = First(); ti; ti = Next(ti)) { + if (!ti->IsPatternTimer()) + TimersModified |= ti->SetEventFromSchedule(Schedules); + } + return TimersModified; +} + +bool cTimers::SpawnPatternTimers(const cSchedules *Schedules) +{ + bool TimersModified = false; + for (cTimer *ti = First(); ti; ti = Next(ti)) { + if (ti->IsPatternTimer() && ti->Local()) { + if (ti->HasFlags(tfActive)) + TimersModified |= ti->SpawnPatternTimers(Schedules, this); + } + } return TimersModified; } @@ -896,6 +1111,7 @@ bool cTimers::DeleteExpired(void) while (ti) { cTimer *next = Next(ti); if (!ti->Remote() && ti->Expired()) { + ti->SetEvent(NULL); // Del() doesn't call ~cTimer() right away, so this is necessary here isyslog("deleting timer %s", *ti->ToDescr()); Del(ti); TimersModified = true; diff --git a/timers.h b/timers.h index ccb64090..79dc39a6 100644 --- a/timers.h +++ b/timers.h @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: timers.h 4.12 2019/05/23 09:47:19 kls Exp $ + * $Id: timers.h 5.1 2020/12/26 15:49:01 kls Exp $ */ #ifndef __TIMERS_H @@ -20,10 +20,14 @@ enum eTimerFlags { tfNone = 0x0000, tfInstant = 0x0002, tfVps = 0x0004, tfRecording = 0x0008, + tfSpawned = 0x0010, + tfAvoid = 0x0020, tfAll = 0xFFFF, }; enum eTimerMatch { tmNone, tmPartial, tmFull }; +class cTimers; + class cTimer : public cListObject { friend class cMenuEditTimer; private: @@ -40,13 +44,14 @@ private: int stop; int priority; int lifetime; + mutable char pattern[NAME_MAX * 2 + 1]; // same size as 'file', to be able to initially fill 'pattern' with 'file' in the 'Edit timer' menu mutable char file[NAME_MAX * 2 + 1]; // *2 to be able to hold 'title' and 'episode', which can each be up to 255 characters long char *aux; char *remote; const cEvent *event; public: cTimer(bool Instant = false, bool Pause = false, const cChannel *Channel = NULL); - cTimer(const cEvent *Event); + cTimer(const cEvent *Event, const char *FileName = NULL, const cTimer *PatternTimer = NULL); cTimer(const cTimer &Timer); virtual ~cTimer(); cTimer& operator= (const cTimer &Timer); @@ -63,12 +68,14 @@ public: int Stop(void) const { return stop; } int Priority(void) const { return priority; } int Lifetime(void) const { return lifetime; } + const char *Pattern(void) const { return pattern; } const char *File(void) const { return file; } time_t FirstDay(void) const { return weekdays ? day : 0; } const char *Aux(void) const { return aux; } const char *Remote(void) const { return remote; } bool Local(void) const { return !remote; } // convenience time_t Deferred(void) const { return deferred; } + cString PatternAndFile(void) const; cString ToText(bool UseChannelID = false) const; cString ToDescr(void) const; const cEvent *Event(void) const { return event; } @@ -80,13 +87,17 @@ public: bool DayMatches(time_t t) const; static time_t IncDay(time_t t, int Days); static time_t SetTime(time_t t, int SecondsFromMidnight); + void SetPattern(const char *Pattern); void SetFile(const char *File); + bool IsPatternTimer(void) const { return *pattern; } bool Matches(time_t t = 0, bool Directly = false, int Margin = 0) const; eTimerMatch Matches(const cEvent *Event, int *Overlap = NULL) const; bool Expired(void) const; time_t StartTime(void) const; time_t StopTime(void) const; void SetId(int Id); + void SpawnPatternTimer(const cEvent *Event, cTimers *Timers); + bool SpawnPatternTimers(const cSchedules *Schedules, cTimers *Timers); bool SetEventFromSchedule(const cSchedules *Schedules); bool SetEvent(const cEvent *Event); void SetRecording(bool Recording); @@ -182,6 +193,7 @@ public: const cTimer *GetNextActiveTimer(void) const; const cTimer *UsesChannel(const cChannel *Channel) const; bool SetEvents(const cSchedules *Schedules); + bool SpawnPatternTimers(const cSchedules *Schedules); bool DeleteExpired(void); void Add(cTimer *Timer, cTimer *After = NULL); void Ins(cTimer *Timer, cTimer *Before = NULL); diff --git a/tools.c b/tools.c index 3a3a1143..046829f0 100644 --- a/tools.c +++ b/tools.c @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: tools.c 4.13 2020/11/22 13:32:05 kls Exp $ + * $Id: tools.c 5.1 2020/12/26 15:49:01 kls Exp $ */ #include "tools.h" @@ -198,6 +198,12 @@ int strcountchr(const char *s, char c) return n; } +const char *strgetlast(const char *s, char c) +{ + const char *p = strrchr(s, c); + return p ? p + 1 : s; +} + char *stripspace(char *s) { if (s && *s) { diff --git a/tools.h b/tools.h index 5d1559a2..912bb88f 100644 --- a/tools.h +++ b/tools.h @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: tools.h 4.18 2020/09/16 13:48:33 kls Exp $ + * $Id: tools.h 5.1 2020/12/26 15:49:01 kls Exp $ */ #ifndef __TOOLS_H @@ -193,6 +193,33 @@ public: static cString vsprintf(const char *fmt, va_list &ap); }; +class cNullTerminate { +private: + char *p; + char c; +public: + cNullTerminate(void) { + p = NULL; + c = 0; + } + cNullTerminate(char *s) { + Set(s); + } + ~cNullTerminate() { + if (p) + *p = c; + } + void Set(char *s) { + if (s) { + p = s; + c = *s; + *s = 0; + } + else + p = NULL; + } + }; + ssize_t safe_read(int filedes, void *buffer, size_t size); ssize_t safe_write(int filedes, const void *buffer, size_t size); void writechar(int filedes, char c); @@ -206,6 +233,7 @@ char *strreplace(char *s, char c1, char c2); char *strreplace(char *s, const char *s1, const char *s2); ///< re-allocates 's' and deletes the original string if necessary! const char *strchrn(const char *s, char c, size_t n); ///< returns a pointer to the n'th occurrence (counting from 1) of c in s, or NULL if no such character was found. If n is 0, s is returned. int strcountchr(const char *s, char c); ///< returns the number of occurrences of 'c' in 's'. +const char *strgetlast(const char *s, char c); // returns the part of 's' after the last occurrence of 'c', or 's' if there is no 'c'. inline char *skipspace(const char *s) { if ((uchar)*s > ' ') // most strings don't have any leading space, so handle this case as fast as possible diff --git a/vdr.1 b/vdr.1 index 0e04d3ba..33f8fd72 100644 --- a/vdr.1 +++ b/vdr.1 @@ -8,7 +8,7 @@ .\" License as specified in the file COPYING that comes with the .\" vdr distribution. .\" -.\" $Id: vdr.1 4.4 2018/04/10 13:58:06 kls Exp $ +.\" $Id: vdr.1 5.1 2020/12/26 15:49:01 kls Exp $ .\" .TH vdr 1 "15 Apr 2018" "2.4" "Video Disk Recorder" .SH NAME @@ -305,6 +305,11 @@ The actual data files of a recording. Contains all current EPG data. Can be used for external processing and will also be read at program startup to have the full EPG data available immediately. .TP +.I donerecs.data +Contains the names of recordings that have been done by pattern timers with '@' +as the first character of the pattern. File names are appended to this file after +a recording has finished, and the entire file is read upon startup of VDR. +.TP .I .update If this file is present in the video directory, its last modification time will be used to trigger an update of the list of recordings in the "Recordings" menu. diff --git a/vdr.5 b/vdr.5 index 777f6af3..8251903b 100644 --- a/vdr.5 +++ b/vdr.5 @@ -8,7 +8,7 @@ .\" License as specified in the file COPYING that comes with the .\" vdr distribution. .\" -.\" $Id: vdr.5 4.8 2018/04/10 13:58:16 kls Exp $ +.\" $Id: vdr.5 5.1 2020/12/26 15:49:01 kls Exp $ .\" .TH vdr 5 "15 Apr 2018" "2.4" "Video Disk Recorder Files" .SH NAME @@ -317,10 +317,12 @@ The individual bits in this field have the following meaning: .TS tab (@); l l. -\fB1\fR@the timer is active (and will record if it hits) -\fB2\fR@this is an instant recording timer -\fB4\fR@this timer uses VPS -\fB8\fR@this timer is currently recording (may only be up-to-date with SVDRP) +\fB0x0001\fR@the timer is active (and will record if it hits) +\fB0x0002\fR@this is an instant recording timer +\fB0x0004\fR@this timer uses VPS +\fB0x0008\fR@this timer is currently recording (may only be up-to-date with SVDRP) +\fB0x0010\fR@this timer was spawned from a pattern timer +\fB0x0020\fR@this timer will store the recording's name in donerecs.data .TE All other bits are reserved for future use. @@ -425,6 +427,37 @@ by the title and episode information from the EPG data at the time of recording (if that data is available). If at the time of recording either of these cannot be determined, \fBTITLE\fR will default to the channel name, and \fBEPISODE\fR will default to a blank. + +The file name can be prepended with a pattern, enclosed in curly braces, as in + +{Columbo}Movies~TITLE + +which makes this a "pattern timer". A pattern timer records every event on the +given channel where the title contains the pattern (case sensitive). +The following special characters can be used in a pattern: +.TS +tab (;); +l l. +\fB^\fR;anchor to the beginning of the event's title +\fB$\fR;anchor to the end of the event's title +\fB*\fR;match every event +\fB@\fR;avoid duplicate recordings +.TE + +If \fB@\fR is used, it must be the very first character of the pattern. +If both \fB@\fR and \fB^\fR are used, \fB@\fR must come first. +If \fB*\fR is used, it must be the only character in the pattern and may only be +prepended with \fB@\fR. + +In addition to TITLE and EPISODE you can use the following macros to compose the file +name (the curly braces are part of the macros): +.TS +tab (@); +l l. +{<}@everything before the matching pattern +{>}@everything after the matching pattern +{=}@the matching pattern itself (just for completeness) +.TE .TP .B Auxiliary data An arbitrary string that can be used by external applications to store any diff --git a/vdr.c b/vdr.c index 709e2f85..8b8cca39 100644 --- a/vdr.c +++ b/vdr.c @@ -22,7 +22,7 @@ * * The project's page is at http://www.tvdr.de * - * $Id: vdr.c 4.34 2020/11/20 13:49:58 kls Exp $ + * $Id: vdr.c 5.1 2020/12/26 15:49:01 kls Exp $ */ #include @@ -784,6 +784,7 @@ int main(int argc, char *argv[]) KeyMacros.Load(AddDirectory(ConfigDirectory, "keymacros.conf"), true); Folders.Load(AddDirectory(ConfigDirectory, "folders.conf")); CamResponsesLoad(AddDirectory(ConfigDirectory, "camresponses.conf"), true); + DoneRecordingsPattern.Load(AddDirectory(CacheDirectory, "donerecs.data")); if (!*cFont::GetFontFileName(Setup.FontOsd)) { const char *msg = "no fonts available - OSD will not show any text!"; @@ -1098,15 +1099,20 @@ int main(int argc, char *argv[]) static cStateKey TimersStateKey; cTimers *Timers = cTimers::GetTimersWrite(TimersStateKey); { + LOCK_CHANNELS_READ; // Channels are needed for spawning pattern timers! // Assign events to timers: static cStateKey SchedulesStateKey; if (TimersStateKey.StateChanged()) SchedulesStateKey.Reset(); // we assign events if either the Timers or the Schedules have changed bool TimersModified = false; if (const cSchedules *Schedules = cSchedules::GetSchedulesRead(SchedulesStateKey)) { - Timers->SetSyncStateKey(StateKeySVDRPRemoteTimersPoll); + Timers->SetSyncStateKey(StateKeySVDRPRemoteTimersPoll); // setting events shall not trigger a remote timer poll... if (Timers->SetEvents(Schedules)) TimersModified = true; + if (Timers->SpawnPatternTimers(Schedules)) { + StateKeySVDRPRemoteTimersPoll.Reset(); // ...but spawning new timers must! + TimersModified = true; + } SchedulesStateKey.Remove(); } TimersStateKey.Remove(TimersModified); // we need to remove the key here, so that syncing StateKeySVDRPRemoteTimersPoll takes effect!