/* -*- c++ -*- Copyright (C) 2004-2012 Christian Wieninger 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. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Or, point your browser to http://www.gnu.org/licenses/old-licenses/gpl-2.0.html The author can be reached at cwieninger@gmx.de The project's page is at http://winni.vdr-developer.org/epgsearch */ #include #include "epgsearchext.h" #include "epgsearchcfg.h" #include "epgsearchcats.h" #include "epgsearchtools.h" #include #include "menu_searchresults.h" #include "menu_dirselect.h" #include "changrp.h" #include "menu_search.h" #include "menu_searchedit.h" #include "menu_recsdone.h" #include "searchtimer_thread.h" #include "timer_thread.h" #include "uservars.h" #include "blacklist.h" #include cSearchExts SearchExts; cSearchExts SearchTemplates; #ifndef MAX_SUBTITLE_LENGTH #define MAX_SUBTITLE_LENGTH 40 #endif // -- cSearchExt ----------------------------------------------------------------- char *cSearchExt::buffer = NULL; cSearchExt::cSearchExt(void) { ID = -1; *search = 0; options = 1; useTime = false; startTime = 0000; stopTime = 2359; useChannel = false; channelMin = Channels.GetByNumber(cDevice::CurrentChannel()); channelMax = Channels.GetByNumber(cDevice::CurrentChannel()); channelGroup = NULL; useCase = false; mode = 0; useTitle = true; useSubtitle = true; useDescription = true; useDuration = false; minDuration = 0; maxDuration = 2359; useAsSearchTimer = false; useDayOfWeek = false; DayOfWeek = 0; buffer = NULL; *directory = 0; useEpisode = 0; Priority = EPGSearchConfig.DefPriority; Lifetime = EPGSearchConfig.DefLifetime; MarginStart = EPGSearchConfig.DefMarginStart; MarginStop = EPGSearchConfig.DefMarginStop; useVPS = false; action = searchTimerActionRecord; useExtEPGInfo = false; contentsFilter = ""; catvalues = (char**) malloc(SearchExtCats.Count() * sizeof(char*)); cSearchExtCat *SearchExtCat = SearchExtCats.First(); int index = 0; while (SearchExtCat) { catvalues[index] = (char*)malloc(MaxFileName); *catvalues[index] = 0; SearchExtCat = SearchExtCats.Next(SearchExtCat); index++; } avoidRepeats = 0; compareTitle = 1; compareSubtitle = 1; compareSummary = 1; compareSummaryMatchInPercent = 90; compareDate = 0; allowedRepeats = 0; catvaluesAvoidRepeat = 0; repeatsWithinDays = 0; delAfterDays = 0; recordingsKeep = 0; switchMinsBefore = 1; pauseOnNrRecordings = 0; blacklistMode = blacklistsOnlyGlobal; // no blacklists blacklists.Clear(); fuzzyTolerance = 1; useInFavorites = 0; menuTemplate = 0; delMode = 0; delAfterCountRecs = 0; delAfterDaysOfFirstRec = 0; useAsSearchTimerFrom = 0; useAsSearchTimerTil = 0; ignoreMissingEPGCats = 0; unmuteSoundOnSwitch = 0; skipRunningEvents = false; } cSearchExt::~cSearchExt(void) { if (buffer) { free(buffer); buffer = NULL; } if (catvalues) { cSearchExtCat *SearchExtCat = SearchExtCats.First(); int index = 0; while (SearchExtCat) { free(catvalues[index]); SearchExtCat = SearchExtCats.Next(SearchExtCat); index++; } free(catvalues); catvalues = NULL; } } cSearchExt& cSearchExt::operator= (const cSearchExt &SearchExt) { CopyFromTemplate(&SearchExt); ID = SearchExt.ID; strcpy(search, SearchExt.search); cSearchExtCat *SearchExtCat = SearchExtCats.First(); int index = 0; while (SearchExtCat) { *catvalues[index] = 0; strcpy(catvalues[index], SearchExt.catvalues[index]); SearchExtCat = SearchExtCats.Next(SearchExtCat); index++; } return *this; } void cSearchExt::CopyFromTemplate(const cSearchExt* templ, bool ignoreChannelSettings) { options = templ->options; useTime = templ->useTime; startTime = templ->startTime; stopTime = templ->stopTime; if (!ignoreChannelSettings) useChannel = templ->useChannel; useCase = templ->useCase; mode = templ->mode; useTitle = templ->useTitle; useSubtitle = templ->useSubtitle; useDescription = templ->useDescription; useDuration = templ->useDuration; minDuration = templ->minDuration; maxDuration = templ->maxDuration; useAsSearchTimer = templ->useAsSearchTimer; useDayOfWeek = templ->useDayOfWeek; DayOfWeek = templ->DayOfWeek; useEpisode = templ->useEpisode; strcpy(directory, templ->directory); Priority = templ->Priority; Lifetime = templ->Lifetime; MarginStart = templ->MarginStart; MarginStop = templ->MarginStop; useVPS = templ->useVPS; action = templ->action; useExtEPGInfo = templ->useExtEPGInfo; contentsFilter = templ->contentsFilter; switchMinsBefore = templ->switchMinsBefore; pauseOnNrRecordings = templ->pauseOnNrRecordings; cSearchExtCat *SearchExtCat = SearchExtCats.First(); int index = 0; while (SearchExtCat) { strcpy(catvalues[index], templ->catvalues[index]); SearchExtCat = SearchExtCats.Next(SearchExtCat); index++; } if (!ignoreChannelSettings) { channelMin = templ->channelMin; channelMax = templ->channelMax; if (channelGroup) { free(channelGroup); channelGroup = NULL; } if (templ->channelGroup) channelGroup = strdup(templ->channelGroup); } avoidRepeats = templ->avoidRepeats; compareTitle = templ->compareTitle; compareSubtitle = templ->compareSubtitle; compareSummary = templ->compareSummary; compareSummaryMatchInPercent = templ->compareSummaryMatchInPercent; compareDate = templ->compareDate; allowedRepeats = templ->allowedRepeats; catvaluesAvoidRepeat = templ->catvaluesAvoidRepeat; repeatsWithinDays = templ->repeatsWithinDays; delAfterDays = templ->delAfterDays; recordingsKeep = templ->recordingsKeep; blacklistMode = templ->blacklistMode; blacklists.Clear(); cBlacklistObject* blacklistObj = templ->blacklists.First(); while(blacklistObj) { blacklists.Add(new cBlacklistObject(blacklistObj->blacklist)); blacklistObj = templ->blacklists.Next(blacklistObj); } fuzzyTolerance = templ->fuzzyTolerance; useInFavorites = templ->useInFavorites; menuTemplate = templ->menuTemplate; delMode = templ->delMode; delAfterCountRecs = templ->delAfterCountRecs; delAfterDaysOfFirstRec = templ->delAfterDaysOfFirstRec; useAsSearchTimerFrom = templ->useAsSearchTimerFrom; useAsSearchTimerTil = templ->useAsSearchTimerTil; ignoreMissingEPGCats = templ->ignoreMissingEPGCats; unmuteSoundOnSwitch = templ->unmuteSoundOnSwitch; } bool cSearchExt::operator< (const cListObject &ListObject) { cSearchExt *SE = (cSearchExt *)&ListObject; return strcasecmp(search, SE->search) < 0; } char* replaceSpecialChars(const char* in) { char* tmp_in = strdup(in); while(strstr(tmp_in, "|")) tmp_in = strreplace(tmp_in, "|", "!^pipe^!"); // ugly: replace a pipe with something, strreplace(tmp_in, ':', '|'); return tmp_in; } const char *cSearchExt::ToText() { char tmp_Start[5] = ""; char tmp_Stop[5] = ""; char tmp_minDuration[5] = ""; char tmp_maxDuration[5] = ""; cString tmp_chanSel; char* tmp_catvalues = NULL; char* tmp_blacklists = NULL; free(buffer); char* tmp_search = replaceSpecialChars(search); char* tmp_directory = replaceSpecialChars(directory); char* tmp_contentsFilter = replaceSpecialChars(contentsFilter.c_str()); if (useTime) { sprintf(tmp_Start, "%04d", startTime); sprintf(tmp_Stop, "%04d", stopTime); } if (useDuration) { sprintf(tmp_minDuration, "%04d", minDuration); sprintf(tmp_maxDuration, "%04d", maxDuration); } if (useChannel==1) { if (channelMin->Number() < channelMax->Number()) tmp_chanSel = cString::sprintf("%s|%s", CHANNELSTRING(channelMin), CHANNELSTRING(channelMax)); else tmp_chanSel = cString(CHANNELSTRING(channelMin)); } if (useChannel==2) { int channelGroupNr = ChannelGroups.GetIndex(channelGroup); if (channelGroupNr == -1) { LogFile.eSysLog("channel group '%s' does not exist!", channelGroup); useChannel = 0; } else tmp_chanSel = cString(channelGroup); } if (useExtEPGInfo) { cSearchExtCat *SearchExtCat = SearchExtCats.First(); int index = 0; while (SearchExtCat) { char* catvalue = NULL; if (msprintf(&catvalue, "%s", catvalues[index])==-1) break; while(strstr(catvalue, ":")) catvalue = strreplace(catvalue, ":", "!^colon^!"); // ugly: replace with something, that should not happen to be part ofa category value while(strstr(catvalue, "|")) catvalue = strreplace(catvalue, "|", "!^pipe^!"); // ugly: replace with something, that should not happen to be part of a regular expression if (index == 0) msprintf(&tmp_catvalues, "%d#%s", SearchExtCat->id, catvalue); else { char* temp = tmp_catvalues; msprintf(&tmp_catvalues, "%s|%d#%s", tmp_catvalues, SearchExtCat->id, catvalue); free(temp); } SearchExtCat = SearchExtCats.Next(SearchExtCat); index++; free(catvalue); } } if (blacklistMode == blacklistsSelection && blacklists.Count() > 0) { cBlacklistObject *blacklistObj = blacklists.First(); int index = 0; while (blacklistObj) { if (index == 0) msprintf(&tmp_blacklists, "%d", blacklistObj->blacklist->ID); else { char* temp = tmp_blacklists; msprintf(&tmp_blacklists, "%s|%d", tmp_blacklists, blacklistObj->blacklist->ID); free(temp); } blacklistObj = blacklists.Next(blacklistObj); index++; } } msprintf(&buffer, "%d:%s:%d:%s:%s:%d:%s:%d:%d:%d:%d:%d:%d:%s:%s:%d:%d:%d:%d:%s:%d:%d:%d:%d:%d:%d:%d:%s:%d:%d:%d:%d:%d:%ld:%d:%d:%d:%d:%d:%d:%s:%d:%d:%d:%d:%d:%d:%ld:%ld:%d:%d:%d:%s:%d", ID, tmp_search, useTime, tmp_Start, tmp_Stop, useChannel, (useChannel>0 && useChannel<3)?*tmp_chanSel:"0", useCase, mode, useTitle, useSubtitle, useDescription, useDuration, tmp_minDuration, tmp_maxDuration, useAsSearchTimer, useDayOfWeek, DayOfWeek, useEpisode, tmp_directory, Priority, Lifetime, MarginStart, MarginStop, useVPS, action, useExtEPGInfo, useExtEPGInfo?tmp_catvalues:"", avoidRepeats, allowedRepeats, compareTitle, compareSubtitle, compareSummary, catvaluesAvoidRepeat, repeatsWithinDays, delAfterDays, recordingsKeep, switchMinsBefore, pauseOnNrRecordings, blacklistMode, blacklists.Count()>0?tmp_blacklists:"", fuzzyTolerance, useInFavorites, menuTemplate, delMode, delAfterCountRecs, delAfterDaysOfFirstRec, useAsSearchTimerFrom, useAsSearchTimerTil, ignoreMissingEPGCats, unmuteSoundOnSwitch, compareSummaryMatchInPercent, contentsFilter.c_str(), compareDate); if (tmp_search) free(tmp_search); if (tmp_directory) free(tmp_directory); if (tmp_catvalues) free(tmp_catvalues); if (tmp_blacklists) free(tmp_blacklists); if (tmp_contentsFilter) free(tmp_contentsFilter); return buffer; } bool cSearchExt::Parse(const char *s) { char *line; char *pos; char *pos_next; int parameter = 1; int valuelen; char value[MaxFileName]; bool disableSearchtimer = false; *directory = 0; *search = 0; pos = line = strdup(s); pos_next = pos + strlen(pos); if (*pos_next == '\n') *pos_next = 0; while (*pos) { while (*pos == ' ') pos++; if (*pos) { if (*pos != ':') { pos_next = strchr(pos, ':'); if (!pos_next) pos_next = pos + strlen(pos); valuelen = pos_next - pos + 1; if (valuelen > MaxFileName) valuelen = MaxFileName; strn0cpy(value, pos, valuelen); pos = pos_next; switch (parameter) { case 1: if (!isnumber(value)) return false; ID = atoi(value); break; case 2: strcpy(search, value); break; case 3: useTime = atoi(value); break; case 4: startTime = atoi(value); break; case 5: stopTime = atoi(value); break; case 6: useChannel = atoi(value); break; case 7: if (useChannel == 0) { channelMin = NULL; channelMax = NULL; } else if (useChannel == 1) { int minNum=0, maxNum=0; int fields = sscanf(value, "%d-%d", &minNum, &maxNum); if (fields == 0) // stored with ID { #ifdef __FreeBSD__ char *channelMinbuffer = MALLOC(char, 32); char *channelMaxbuffer = MALLOC(char, 32); int channels = sscanf(value, "%31[^|]|%31[^|]", channelMinbuffer, channelMaxbuffer); #else char *channelMinbuffer = NULL; char *channelMaxbuffer = NULL; int channels = sscanf(value, "%a[^|]|%a[^|]", &channelMinbuffer, &channelMaxbuffer); #endif channelMin = Channels.GetByChannelID(tChannelID::FromString(channelMinbuffer), true, true); if (!channelMin) { LogFile.eSysLog("ERROR: channel '%s' not defined", channelMinbuffer); channelMin = channelMax = NULL; disableSearchtimer = true; useChannel = 0; } if (channels == 1) channelMax = channelMin; else { channelMax = Channels.GetByChannelID(tChannelID::FromString(channelMaxbuffer), true, true); if (!channelMax) { LogFile.eSysLog("ERROR: channel '%s' not defined", channelMaxbuffer); channelMin = channelMax = NULL; disableSearchtimer = true; useChannel = 0; } } free(channelMinbuffer); free(channelMaxbuffer); } } else if (useChannel == 2) channelGroup = strdup(value); break; case 8: useCase = atoi(value); break; case 9: mode = atoi(value); break; case 10: useTitle = atoi(value); break; case 11: useSubtitle = atoi(value); break; case 12: useDescription = atoi(value); break; case 13: useDuration = atoi(value); break; case 14: minDuration = atoi(value); break; case 15: maxDuration = atoi(value); break; case 16: useAsSearchTimer = atoi(value); break; case 17: useDayOfWeek = atoi(value); break; case 18: DayOfWeek = atoi(value); break; case 19: useEpisode = atoi(value); break; case 20: strcpy(directory, value); break; case 21: Priority = atoi(value); break; case 22: Lifetime = atoi(value); break; case 23: MarginStart = atoi(value); break; case 24: MarginStop = atoi(value); break; case 25: useVPS = atoi(value); break; case 26: action = atoi(value); break; case 27: useExtEPGInfo = atoi(value); break; case 28: if (!ParseExtEPGValues(value)) { LogFile.eSysLog("ERROR reading ext. EPG values - 1"); free(line); return false; } break; case 29: avoidRepeats = atoi(value); break; case 30: allowedRepeats = atoi(value); break; case 31: compareTitle = atoi(value); break; case 32: compareSubtitle = atoi(value)>0?1:0; break; case 33: compareSummary = atoi(value); break; case 34: catvaluesAvoidRepeat = atol(value); break; case 35: repeatsWithinDays = atoi(value); break; case 36: delAfterDays = atoi(value); break; case 37: recordingsKeep = atoi(value); break; case 38: switchMinsBefore = atoi(value); break; case 39: pauseOnNrRecordings = atoi(value); break; case 40: blacklistMode = atoi(value); break; case 41: if (blacklistMode == blacklistsSelection && !ParseBlacklistIDs(value)) { LogFile.eSysLog("ERROR parsing blacklist IDs"); free(line); return false; } break; case 42: fuzzyTolerance = atoi(value); break; case 43: useInFavorites = atoi(value); break; case 44: menuTemplate = atoi(value); break; case 45: delMode = atoi(value); break; case 46: delAfterCountRecs = atoi(value); break; case 47: delAfterDaysOfFirstRec = atoi(value); break; case 48: useAsSearchTimerFrom = atol(value); break; case 49: useAsSearchTimerTil = atol(value); break; case 50: ignoreMissingEPGCats = atoi(value); break; case 51: unmuteSoundOnSwitch = atoi(value); break; case 52: compareSummaryMatchInPercent = atoi(value); break; case 53: contentsFilter = value; break; case 54: compareDate = atoi(value); break; default: break; } //switch } parameter++; } if (*pos) pos++; } //while strreplace(directory, '|', ':'); strreplace(search, '|', ':'); strreplace(contentsFilter, "|", ":"); while(strstr(search, "!^pipe^!")) strreplace(search, "!^pipe^!", "|"); while(strstr(directory, "!^pipe^!")) strreplace(directory, "!^pipe^!", "|"); strreplace(contentsFilter, "!^pipe^!", "|"); if (disableSearchtimer && useAsSearchTimer) { useAsSearchTimer = false; LogFile.Log(1, "search timer '%s' disabled", search); } free(line); return (parameter >= 11) ? true : false; } char* cSearchExt::BuildFile(const cEvent* pEvent) const { char* file = NULL; if (!pEvent) return file; const char *Subtitle = pEvent ? pEvent->ShortText() : NULL; char SubtitleBuffer[Utf8BufSize(MAX_SUBTITLE_LENGTH)]; if (isempty(Subtitle)) { time_t Start = pEvent->StartTime(); struct tm tm_r; strftime(SubtitleBuffer, sizeof(SubtitleBuffer), "%Y.%m.%d-%R-%a", localtime_r(&Start, &tm_r)); Subtitle = SubtitleBuffer; } else if (Utf8StrLen(Subtitle) > MAX_SUBTITLE_LENGTH) { Utf8Strn0Cpy(SubtitleBuffer, Subtitle, sizeof(SubtitleBuffer)); SubtitleBuffer[Utf8SymChars(SubtitleBuffer, MAX_SUBTITLE_LENGTH)] = 0; Subtitle = SubtitleBuffer; } if (useEpisode) { cString pFile = cString::sprintf("%s~%s", pEvent->Title(), Subtitle); if (file) free(file); file = strdup(pFile); } else if (pEvent->Title()) file = strdup(pEvent->Title()); if (!isempty(directory)) { char* pFile = NULL; cVarExpr varExprDir(directory); if (!varExprDir.DependsOnVar("%title%", pEvent) && !varExprDir.DependsOnVar("%subtitle%", pEvent)) msprintf(&pFile, "%s~%s", directory, file?file:""); else // ignore existing title and subtitle in file if already used as variables in directory msprintf(&pFile, "%s", directory); // parse the epxression and evaluate it cVarExpr varExprFile(pFile); if (pFile) free(pFile); pFile = strdup(varExprFile.Evaluate(pEvent).c_str()); cVarExpr varExprSearchFile(pFile); if (pFile) free(pFile); pFile = strdup(varExprSearchFile.Evaluate(this).c_str()); if (file) free(file); file = strdup(pFile); free(pFile); } // replace some special chars if (file) { while(strstr(file, "|")) file = strreplace(file, "|", "!^pipe^!"); while(strstr(file, ":")) file = strreplace(file, ':', '|'); while(strstr(file, " ~")) file = strreplace(file, " ~", "~"); while(strstr(file, "~ ")) file = strreplace(file, "~ ", "~"); } return file; } bool cSearchExt::ParseBlacklistIDs(const char *s) { char *line; char *pos; char *pos_next; int valuelen; char value[MaxFileName]; cMutexLock BlacklistLock(&Blacklists); blacklists.Clear(); pos = line = strdup(s); pos_next = pos + strlen(pos); if (*pos_next == '\n') *pos_next = 0; while (*pos) { while (*pos == ' ') pos++; if (*pos) { if (*pos != '|') { pos_next = strchr(pos, '|'); if (!pos_next) pos_next = pos + strlen(pos); valuelen = pos_next - pos + 1; if (valuelen > MaxFileName) valuelen = MaxFileName; strn0cpy(value, pos, valuelen); pos = pos_next; cBlacklist* blacklist = Blacklists.GetBlacklistFromID(atoi(value)); if (!blacklist) LogFile.eSysLog("blacklist ID %s missing, will be skipped", value); else blacklists.Add(new cBlacklistObject(blacklist)); } } if (*pos) pos++; } //while free(line); return true; } bool cSearchExt::ParseExtEPGValues(const char *s) { char *line; char *pos; char *pos_next; int valuelen; char value[MaxFileName]; pos = line = strdup(s); pos_next = pos + strlen(pos); if (*pos_next == '\n') *pos_next = 0; while (*pos) { while (*pos == ' ') pos++; if (*pos) { if (*pos != '|') { pos_next = strchr(pos, '|'); if (!pos_next) pos_next = pos + strlen(pos); valuelen = pos_next - pos + 1; if (valuelen > MaxFileName) valuelen = MaxFileName; strn0cpy(value, pos, valuelen); pos = pos_next; if (!ParseExtEPGEntry(value)) { LogFile.eSysLog("ERROR reading ext. EPG value: %s", value); free(line); return false; } } } if (*pos) pos++; } //while free(line); return true; } bool cSearchExt::ParseExtEPGEntry(const char *s) { char *line; char *pos; char *pos_next; int parameter = 1; int valuelen; char value[MaxFileName]; int currentid = -1; pos = line = strdup(s); pos_next = pos + strlen(pos); if (*pos_next == '\n') *pos_next = 0; while (*pos) { while (*pos == ' ') pos++; if (*pos) { if (*pos != '#') { pos_next = strchr(pos, '#'); if (!pos_next) pos_next = pos + strlen(pos); valuelen = pos_next - pos + 1; if (valuelen > MaxFileName) valuelen = MaxFileName; strn0cpy(value, pos, valuelen); pos = pos_next; switch (parameter) { case 1: { currentid = atoi(value); int index = SearchExtCats.GetIndexFromID(currentid); if (index > -1 && index < SearchExtCats.Count()) strcpy(catvalues[index], ""); } break; case 2: if (currentid > -1) { int index = SearchExtCats.GetIndexFromID(currentid); if (index > -1 && index < SearchExtCats.Count()) { while(strstr(value, "!^colon^!")) strreplace(value, "!^colon^!", ":"); while(strstr(value, "!^pipe^!")) strreplace(value, "!^pipe^!", "|"); strcpy(catvalues[index], value); } } break; default: break; } //switch } parameter++; } if (*pos) pos++; } //while free(line); return (parameter >= 2) ? true : false; } bool cSearchExt::Save(FILE *f) { return fprintf(f, "%s\n", ToText()) > 0; } cEvent * cSearchExt::GetEventBySearchExt(const cSchedule *schedules, const cEvent *Start, bool inspectTimerMargin) { if (!schedules) return NULL; cEvent *pe = NULL; cEvent *p1 = NULL; const cList* Events = schedules->Events(); if (Start) p1 = Events->Next(Start); else p1 = Events->First(); time_t tNow=time(NULL); char* searchText = strdup(search); int searchStart = 0, searchStop = 0; if (useTime) { searchStart = startTime; searchStop = stopTime; if (searchStop < searchStart) searchStop += 2400; } int minSearchDuration = 0; int maxSearchDuration = 0; if (useDuration) { minSearchDuration = minDuration/100*60 + minDuration%100; maxSearchDuration = maxDuration/100*60 + maxDuration%100; } if (!useCase) ToLower(searchText); for (cEvent *p = p1; p; p = Events->Next(p)) { if(!p) { break; } if (skipRunningEvents && tNow > p->StartTime()) continue; // ignore events without title if (!p->Title() || !*p->Title()) continue; if (tNow < p->EndTime() + (inspectTimerMargin?(MarginStop * 60):0)) { if (useTime) { time_t tEvent = p->StartTime(); struct tm tmEvent; localtime_r(&tEvent, &tmEvent); int eventStart = tmEvent.tm_hour*100 + tmEvent.tm_min; int eventStart2 = eventStart + 2400; if ((eventStart < searchStart || eventStart > searchStop) && (eventStart2 < searchStart || eventStart2 > searchStop)) continue; if (useDayOfWeek) { if (DayOfWeek >= 0) { if (( DayOfWeek != tmEvent.tm_wday || (DayOfWeek == tmEvent.tm_wday && eventStart < searchStart)) && (!((DayOfWeek+1)%7 == tmEvent.tm_wday && eventStart2 < searchStop))) continue; } else { int iFound = 0; for(int i=0; i<7; i++) { if ((abs(DayOfWeek) & (int)pow(2,i)) && ((i == tmEvent.tm_wday && eventStart >= searchStart) || ((i+1)%7 == tmEvent.tm_wday && eventStart2 < searchStop))) { iFound = 1; break; } } if (!iFound) continue; } } } if (useDuration) { int duration = p->Duration()/60; if (minSearchDuration > duration || maxSearchDuration < duration) continue; } if (!useTime && useDayOfWeek) { time_t tEvent = p->StartTime(); struct tm tmEvent; localtime_r(&tEvent, &tmEvent); if (DayOfWeek >= 0 && DayOfWeek != tmEvent.tm_wday) continue; if (DayOfWeek < 0) { int iFound = 0; for(int i=0; i<7; i++) if (abs(DayOfWeek) & (int)pow(2,i) && i == tmEvent.tm_wday) { iFound = 1; break; } if (!iFound) continue; } } char* szTest = NULL; msprintf(&szTest, "%s%s%s%s%s", (useTitle?(p->Title()?p->Title():""):""), (useSubtitle||useDescription)?"~":"", (useSubtitle?(p->ShortText()?p->ShortText():""):""),useDescription?"~":"", (useDescription?(p->Description()?p->Description():""):"")); if (!useCase) ToLower(szTest); if (szTest && *szTest) { if (!MatchesSearchMode(szTest, searchText, mode," ,;|~", fuzzyTolerance)) { free(szTest); continue; } } if (szTest) free(szTest); if (contentsFilter.size() > 0 && !MatchesContentsFilter(p)) continue; if (useExtEPGInfo && !MatchesExtEPGInfo(p)) continue; pe=p; break; } } free(searchText); return pe; } // returns a pointer array to the matching search results cSearchResults* cSearchExt::Run(int PayTVMode, bool inspectTimerMargin, int evalLimitMins, cSearchResults* pPrevResults, bool suppressRepeatCheck) { LogFile.Log(3,"start search for search timer '%s'", search); cSchedulesLock schedulesLock; const cSchedules *schedules; schedules = cSchedules::Schedules(schedulesLock); if(!schedules) { LogFile.Log(1,"schedules are currently locked! try again later."); return NULL; } bool noPayTV = false; if (PayTVMode == -1) // use search's setting noPayTV = (useChannel == 3); else noPayTV = (PayTVMode == 1); time_t tNow=time(NULL); const cSchedule *Schedule = schedules->First(); cSearchResults* pSearchResults = pPrevResults; cSearchResults* pBlacklistResults = GetBlacklistEvents(inspectTimerMargin?MarginStop:0); int counter = 0; while (Schedule) { cChannel* channel = Channels.GetByChannelID(Schedule->ChannelID(),true,true); if (!channel) { Schedule = (const cSchedule *)schedules->Next(Schedule); continue; } if (useChannel == 1 && channelMin && channelMax) { if (channelMin->Number() > channel->Number() || channelMax->Number() < channel->Number()) { Schedule = (const cSchedule *)schedules->Next(Schedule); continue; } } if (useChannel == 2 && channelGroup) { cChannelGroup* group = ChannelGroups.GetGroupByName(channelGroup); if (!group || !group->ChannelInGroup(channel)) { Schedule = (const cSchedule *)schedules->Next(Schedule); continue; } } if (useChannel == 3 && noPayTV) { if (channel->Ca() >= CA_ENCRYPTED_MIN) { Schedule = (const cSchedule *)schedules->Next(Schedule); continue; } } if (noPayTV) // no paytv { if (channel->Ca() >= CA_ENCRYPTED_MIN) { Schedule = (const cSchedule *)schedules->Next(Schedule); continue; } } const cEvent *pPrevEvent = NULL; do { const cEvent* event = GetEventBySearchExt(Schedule, pPrevEvent,inspectTimerMargin); pPrevEvent = event; if (evalLimitMins && event) // limit evaluation to now + limit { if (tNow + evalLimitMins*60 <= event->EndTime()) break; } if (event && Channels.GetByChannelID(event->ChannelID(),true,true)) { if (pBlacklistResults && pBlacklistResults->Lookup(event)) { LogFile.Log(3,"skip '%s~%s' (%s - %s, channel %d): matches blacklist", event->Title()?event->Title():"no title", event->ShortText()?event->ShortText():"no subtitle", GETDATESTRING(event), GETTIMESTRING(event), ChannelNrFromEvent(event)); continue; } if (!pSearchResults) pSearchResults = new cSearchResults; pSearchResults->Add(new cSearchResult(event, this)); counter++; } } while(pPrevEvent); Schedule = (const cSchedule *)schedules->Next(Schedule); } LogFile.Log(3,"found %d event(s) for search timer '%s'", counter, search); if (pBlacklistResults) delete pBlacklistResults; if (useAsSearchTimer && avoidRepeats && pSearchResults && !suppressRepeatCheck) { pSearchResults->SortBy(CompareEventTime); // sort before checking repeats to make sure the first event is selected CheckRepeatTimers(pSearchResults); } skipRunningEvents = false; return pSearchResults; } cSearchResults* cSearchExt::GetBlacklistEvents(int MarginStop) { if (blacklistMode == blacklistsNone) return NULL; cMutexLock BlacklistLock(&Blacklists); cSearchResults* blacklistEvents = NULL; if (blacklistMode == blacklistsOnlyGlobal) { cBlacklist* tmpblacklist = Blacklists.First(); while(tmpblacklist) { if (tmpblacklist->isGlobal) blacklistEvents = tmpblacklist->Run(blacklistEvents, MarginStop); tmpblacklist = Blacklists.Next(tmpblacklist); } } if (blacklistMode == blacklistsAll) { cBlacklist* tmpblacklist = Blacklists.First(); while(tmpblacklist) { blacklistEvents = tmpblacklist->Run(blacklistEvents, MarginStop); tmpblacklist = Blacklists.Next(tmpblacklist); } } if (blacklistMode == blacklistsSelection) { cBlacklistObject* tmpblacklistObj = blacklists.First(); while(tmpblacklistObj) { blacklistEvents = tmpblacklistObj->blacklist->Run(blacklistEvents, MarginStop); tmpblacklistObj = blacklists.Next(tmpblacklistObj); } } return blacklistEvents; } void cSearchExt::CheckRepeatTimers(cSearchResults* pResults) { if (!pResults) return; if (avoidRepeats == 0) return; LogFile.Log(2,"analysing repeats for search timer '%s'...", search); if (action != searchTimerActionRecord) { LogFile.Log(3,"search timer not set to 'record', so skip all"); return; } cSearchResult* pResultObj = NULL; for (pResultObj = pResults->First(); pResultObj; pResultObj = pResults->Next(pResultObj)) { if (action != searchTimerActionRecord) // only announce if there is no timer for the event { pResultObj->needsTimer = false; continue; } const cEvent* pEvent = pResultObj->event; // check if this event was already recorded int records = 0; cRecDone* firstRec = NULL; LogFile.Log(3,"get count recordings with %d%% match", compareSummaryMatchInPercent); records = RecsDone.GetCountRecordings(pEvent, this, &firstRec, compareSummaryMatchInPercent); LogFile.Log(3,"recordings: %d", records); if (records > allowedRepeats) // already recorded { LogFile.Log(3,"skip '%s~%s' (%s - %s, channel %d): already recorded %d equal event(s)", pEvent->Title()?pEvent->Title():"no title", pEvent->ShortText()?pEvent->ShortText():"no subtitle", GETDATESTRING(pEvent), GETTIMESTRING(pEvent), ChannelNrFromEvent(pEvent), records); pResultObj->needsTimer = false; // first assume we need no timer continue; } int plannedTimers = 0; LogFile.Log(3,"get planned recordings"); cSearchResult* pFirstResultMatching = NULL; // check other results, if they are already planned for equal events for (cSearchResult* pResultObjP = pResults->First(); pResultObjP; pResultObjP = pResults->Next(pResultObjP)) { if (pResultObj == pResultObjP) break; const cEvent* pEventP = pResultObjP->event; if (!pEventP) continue; if (!pResultObjP->needsTimer) continue; if (EventsMatch(pEvent, pEventP, compareTitle, compareSubtitle, compareSummary, compareDate, catvaluesAvoidRepeat)) { if (!pFirstResultMatching) pFirstResultMatching = pResultObjP; plannedTimers++; } } LogFile.Log(3,"planned: %d", plannedTimers); if (plannedTimers + records > allowedRepeats) { LogFile.Log(3,"skip '%s~%s' (%s - %s, channel %d): events planned(%d), recorded(%d), allowed(%d)", pEvent->Title()?pEvent->Title():"no title", pEvent->ShortText()?pEvent->ShortText():"no subtitle", GETDATESTRING(pEvent), GETTIMESTRING(pEvent), ChannelNrFromEvent(pEvent), plannedTimers, records, allowedRepeats); pResultObj->needsTimer = false; continue; } else if (allowedRepeats > 0 && repeatsWithinDays > 0) // if we only allow repeats with in a given range { if (firstRec) // already recorded, check for allowed repeat within days { if (firstRec->startTime > pEvent->StartTime() - pEvent->Duration()) // no repeat { LogFile.Log(3,"skip '%s~%s' (%s - %s, channel %d); no repeat for event already recorded at %s, channel %d", pEvent->Title()?pEvent->Title():"no title", pEvent->ShortText()?pEvent->ShortText():"no subtitle", GETDATESTRING(pEvent), GETTIMESTRING(pEvent), ChannelNrFromEvent(pEvent), DAYDATETIME(firstRec->startTime), firstRec->ChannelNr()); pResultObj->needsTimer = false; continue; } int daysFromFirstRec = int(double((pEvent->StartTime() - firstRec->startTime)) / (60*60*24) + 0.5); if (daysFromFirstRec > repeatsWithinDays) { LogFile.Log(3,"skip '%s~%s' (%s - %s, channel %d); first recording at %s is %d days before, limit is %d days", pEvent->Title()?pEvent->Title():"no title", pEvent->ShortText()?pEvent->ShortText():"no subtitle", GETDATESTRING(pEvent), GETTIMESTRING(pEvent), ChannelNrFromEvent(pEvent), DAYDATETIME(firstRec->startTime),daysFromFirstRec, repeatsWithinDays); pResultObj->needsTimer = false; continue; } } if (plannedTimers > 0 && pFirstResultMatching) { const cEvent* pFirst = pFirstResultMatching->event; if (pFirst->StartTime() > pEvent->StartTime() - pEvent->Duration()) // no repeat { LogFile.Log(3,"skip '%s~%s' (%s - %s, channel %d); no repeat for event already recorded at %s - %s, channel %d", pEvent->Title()?pEvent->Title():"no title", pEvent->ShortText()?pEvent->ShortText():"no subtitle", GETDATESTRING(pEvent), GETTIMESTRING(pEvent), ChannelNrFromEvent(pEvent), GETDATESTRING(pFirst), GETTIMESTRING(pFirst), ChannelNrFromEvent(pFirst)); pResultObj->needsTimer = false; continue; } int daysBetween = int(double((pEvent->StartTime() - pFirst->StartTime())) / (60*60*24) + 0.5); if (daysBetween > repeatsWithinDays) { LogFile.Log(3,"skip '%s~%s' (%s - %s, channel %d); first event '%s~%s' (%s - %s) is %d days before, limit is %d days", pEvent->Title()?pEvent->Title():"no title", pEvent->ShortText()?pEvent->ShortText():"no subtitle", GETDATESTRING(pEvent), GETTIMESTRING(pEvent), ChannelNrFromEvent(pEvent), GETDATESTRING(pFirst), GETTIMESTRING(pFirst),daysBetween, repeatsWithinDays); pResultObj->needsTimer = false; continue; } } } bool dummy; cTimer* timer = cSearchTimerThread::GetTimer(this, pEvent, dummy); if (timer && !timer->HasFlags(tfActive)) { LogFile.Log(3,"skip '%s~%s' (%s - %s, channel %d), existing timer disabled", pEvent->Title()?pEvent->Title():"no title", pEvent->ShortText()?pEvent->ShortText():"no subtitle", GETDATESTRING(pEvent), GETTIMESTRING(pEvent), ChannelNrFromEvent(pEvent)); pResultObj->needsTimer = false; continue; } else LogFile.Log(3,"*** planning event '%s~%s' (%s - %s, channel %d) for recording", pEvent->Title()?pEvent->Title():"no title", pEvent->ShortText()?pEvent->ShortText():"no subtitle", GETDATESTRING(pEvent), GETTIMESTRING(pEvent), ChannelNrFromEvent(pEvent)); } int needsTimer = 0; for (pResultObj = pResults->First(); pResultObj; pResultObj = pResults->Next(pResultObj)) if (pResultObj->needsTimer) needsTimer++; LogFile.Log(2,"%d/%d events need a timer for search timer '%s'", needsTimer, pResults->Count(), search); } void cSearchExt::CheckExistingRecordings(cSearchResults* pResults) { if (!pResults) return; LogFile.Log(3,"analysing existing recordings for search timer '%s'...", search); // how many recordings do we already have? int num = GetCountRecordings(); cSearchResult* pResultObj = NULL; int remain = pauseOnNrRecordings - num; for (pResultObj = pResults->First(); pResultObj; pResultObj = pResults->Next(pResultObj), remain--) { if (!pResultObj->needsTimer) { remain++; continue; // maybe already disabled because of done feature } pResultObj->needsTimer = (remain > 0); if (remain <= 0) { const cEvent* pEvent = pResultObj->event; LogFile.Log(3,"skip '%s~%s' (%s - %s, channel %d): only %d recordings are allowed", pEvent->Title()?pEvent->Title():"no title", pEvent->ShortText()?pEvent->ShortText():"no subtitle", GETDATESTRING(pEvent), GETTIMESTRING(pEvent), ChannelNrFromEvent(pEvent), pauseOnNrRecordings); } } } bool cSearchExt::MatchesExtEPGInfo(const cEvent* e) { if (!e || !e->Description()) return false; cSearchExtCat* SearchExtCat = SearchExtCats.First(); while (SearchExtCat) { char* value = NULL; int index = SearchExtCats.GetIndexFromID(SearchExtCat->id); if (index > -1) value = catvalues[index]; if (value && SearchExtCat->searchmode >= 10 && atol(value) == 0) // numerical value != 0 ? value = NULL; if (value && *value) { char* testvalue = GetExtEPGValue(e, SearchExtCat); if (!testvalue) return (ignoreMissingEPGCats?true:false); // compare not case sensitive char* valueLower = strdup(value); ToLower(valueLower); ToLower(testvalue); if (!MatchesSearchMode(testvalue, valueLower, SearchExtCat->searchmode, ",;|~", fuzzyTolerance)) { free(testvalue); free(valueLower); return false; } free(testvalue); free(valueLower); } SearchExtCat = SearchExtCats.Next(SearchExtCat); } return true; } void cSearchExt::OnOffTimers(bool bOn) { for (cTimer *ti = Timers.First(); ti; ti = Timers.Next(ti)) { if (((!bOn && ti->HasFlags(tfActive)) || (bOn && !ti->HasFlags(tfActive))) && TriggeredFromSearchTimerID(ti) == ID) ti->OnOff(); } Timers.SetModified(); } void cSearchExt::DeleteAllTimers() { cList DelTimers; cTimer *ti = Timers.First(); while(ti) { if (!ti->Recording() && TriggeredFromSearchTimerID(ti) == ID) { cTimer* tiNext = Timers.Next(ti); LogFile.iSysLog("deleting timer %s", *ti->ToDescr()); Timers.Del(ti); Timers.SetModified(); ti = tiNext; } else ti = Timers.Next(ti); }; } cTimerObjList* cSearchExt::GetTimerList(cTimerObjList* timerList) { if (!timerList) timerList = new cTimerObjList; for (cTimer *ti = Timers.First(); ti; ti = Timers.Next(ti)) { if (TriggeredFromSearchTimerID(ti) == ID) { // check if already in list bool found = false; for (cTimerObj *tObj = timerList->First(); tObj; tObj = timerList->Next(tObj)) { if (tObj->timer == ti) { found = true; break; } } if (!found) timerList->Add(new cTimerObj(ti)); } } return timerList; } // counts the currently existent recordings triggered by this search timer int cSearchExt::GetCountRecordings() { int countRecs = 0; for (cRecording *recording = Recordings.First(); recording; recording = Recordings.Next(recording)) { if (recording->IsEdited()) continue; // ignore recordings edited if (!recording->Info()) continue; char* searchID = GetAuxValue(recording, "s-id"); if (!searchID) continue; if (ID == atoi(searchID)) countRecs++; free(searchID); } LogFile.Log(3, "found %d recordings for search '%s'", countRecs, search); return countRecs; } bool cSearchExt::IsActiveAt(time_t t) { if (useAsSearchTimer == 0) return false; if (useAsSearchTimer == 2) { if (useAsSearchTimerFrom > 0 && t < useAsSearchTimerFrom) return false; if (useAsSearchTimerTil > 0 && t > useAsSearchTimerTil) return false; } return true; } bool cSearchExt::HasContent(int contentID) { for(unsigned int i=0; i>std::noshowbase>>std::hex>>tmpContentID)) return false; if (contentID == tmpContentID) return true; } return false; } void cSearchExt::SetContentFilter(int* contentStringsFlags) { // create the hex array of content descriptor IDs string tmp; contentsFilter = ""; for(unsigned int i=0; contentStringsFlags && i<=CONTENT_DESCRIPTOR_MAX; i++) { if (contentStringsFlags[i]) { std::ostringstream oss; oss<>std::hex>>searchContentID)) return false; int c=0, eventContentID=0; bool found = false; while((eventContentID=e->Contents(c++)) > 0) if (eventContentID == searchContentID) { found = true; break; } if (!found) return false; } return true; #endif } // -- cSearchExts ---------------------------------------------------------------- bool cSearchExts::Load(const char *FileName) { cMutexLock SearchExtsLock(this); Clear(); if (FileName) { free(fileName); fileName = strdup(FileName); } bool result = true; if (fileName && access(fileName, F_OK) == 0) { LogFile.iSysLog("loading %s", fileName); FILE *f = fopen(fileName, "r"); if (f) { int line = 0; char buffer[MAXPARSEBUFFER]; result = true; while (fgets(buffer, sizeof(buffer), f) > 0) { line++; char *p = strchr(buffer, '#'); if (p == buffer) *p = 0; stripspace(buffer); if (!isempty(buffer)) { cSearchExt* search = new cSearchExt; if (search->Parse(buffer)) Add(search); else { LogFile.eSysLog("error in '%s', line %d\n", fileName, line); delete search; result = false; break; } } } fclose(f); } else { LOG_ERROR_STR(fileName); result = false; } } if (!result) fprintf(stderr, "vdr: error while reading '%s'\n", fileName); LogFile.Log(2,"loaded searches from %s (count: %d)", fileName, Count()); return result; } int cSearchExts::GetNewID() { cMutexLock SearchExtsLock(this); int newID = -1; cSearchExt *l = (cSearchExt *)First(); while (l) { newID = max(newID, l->ID); l = (cSearchExt *)l->Next(); } return newID+1; } void cSearchExts::Update(void) { cMutexLock SearchExtsLock(this); cSearchExt *l = (cSearchExt *)First(); while (l) { // check if ID is set if (l->ID == -1) l->ID = GetNewID(); l = (cSearchExt *)l->Next(); } } bool cSearchExts::Save(void) { cMutexLock SearchExtsLock(this); bool result = true; cSearchExt *l = (cSearchExt *)this->First(); cSafeFile f(fileName); if (f.Open()) { while (l) { if (!l->Save(f)) { result = false; break; } l = (cSearchExt *)l->Next(); } if (!f.Close()) result = false; } else result = false; return result; } cSearchExt* cSearchExts::GetSearchFromID(int ID) { if (ID == -1) return NULL; cMutexLock SearchExtsLock(this); cSearchExt *l = (cSearchExt *)First(); while (l) { if (l->ID == ID) return l; l = (cSearchExt *)l->Next(); } return NULL; } void cSearchExts::RemoveBlacklistID(int ID) { bool changed = false; cMutexLock SearchExtsLock(this); cSearchExt *l = (cSearchExt *)First(); while (l) { cBlacklistObject* blacklistObj = l->blacklists.First(); while(blacklistObj) { cBlacklistObject* blacklistObjNext = l->blacklists.Next(blacklistObj); if (blacklistObj->blacklist->ID == ID) { l->blacklists.Del(blacklistObj); changed = true; } blacklistObj = blacklistObjNext; } l = (cSearchExt *)l->Next(); } if (changed) Save(); } bool cSearchExts::Exists(const cSearchExt* SearchExt) { cMutexLock SearchExtsLock(this); cSearchExt *l = (cSearchExt *)First(); while (l) { if (l == SearchExt) return true; l = (cSearchExt *)l->Next(); } return false; } cSearchExts* cSearchExts::Clone() { cSearchExts* clonedList = new cSearchExts(); cMutexLock SearchExtsLock(this); cSearchExt *l = (cSearchExt *)First(); while (l) { cSearchExt* clone = new cSearchExt(); *clone = *l; clonedList->Add(clone); l = (cSearchExt *)l->Next(); } return clonedList; } bool cSearchExts::CheckForAutoDelete(cSearchExt* SearchExt) { if (!SearchExt || SearchExt->delMode == 0) return false; cRecDone* firstRec = NULL; bool delSearch = false; int recs = RecsDone.GetTotalCountRecordings(SearchExt, &firstRec); if (SearchExt->delMode == 1 && SearchExt->delAfterCountRecs > 0) delSearch = recs >= SearchExt->delAfterCountRecs; if (SearchExt->delMode == 2 && SearchExt->delAfterDaysOfFirstRec && firstRec) delSearch = (time(NULL) - firstRec->startTime) > SearchExt->delAfterDaysOfFirstRec * 24 * 60 * 60; if (delSearch) { int DelID = SearchExt->ID; LogFile.Log(1,"auto deleting search '%s' (ID: %d)", SearchExt->search, DelID); cMutexLock SearchExtsLock(&SearchExts); SearchExts.Del(SearchExt); SearchExts.Save(); RecsDone.RemoveSearchID(DelID); } return delSearch; } void cSearchExts::SortBy(int(*compar)(const void *, const void *)) { int n = Count(); cListObject *a[n]; cListObject *object = objects; int i = 0; while (object && i < n) { a[i++] = object; object = object->Next(); } qsort(a, n, sizeof(cListObject *), compar); objects = lastObject = NULL; for (i = 0; i < n; i++) { a[i]->Unlink(); count--; Add(a[i]); } } cSearchResult::cSearchResult(const cEvent* Event, int searchID) : event(Event), blacklist(NULL), needsTimer(true) { search = SearchExts.GetSearchFromID(searchID); }