/* -*- c++ -*- Copyright (C) 2004-2013 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; { LOCK_CHANNELS_READ; 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(); const 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, "%m[^|]|%m[^|]", &channelMinbuffer, &channelMaxbuffer); #endif LOCK_CHANNELS_READ; 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); 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; } const cEvent * cSearchExt::GetEventBySearchExt(const cSchedule *schedules, const cEvent *Start, bool inspectTimerMargin) { if (!schedules) return NULL; const cEvent *pe = NULL; const 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 (const 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); bool noPayTV = false; if (PayTVMode == -1) // use search's setting noPayTV = (useChannel == 3); else noPayTV = (PayTVMode == 1); time_t tNow = time(NULL); cSearchResults* pSearchResults = pPrevResults; cSearchResults* pBlacklistResults = GetBlacklistEvents(inspectTimerMargin ? MarginStop : 0); { LOCK_CHANNELS_READ; LOCK_SCHEDULES_READ; int counter = 0; const cSchedule *Schedule = Schedules->First(); while (Schedule) { const cChannel* channel = Channels->GetByChannelID(Schedule->ChannelID(), true, true); if (!channel) { Schedule = Schedules->Next(Schedule); continue; } if (useChannel == 1 && channelMin && channelMax) { if (channelMin->Number() > channel->Number() || channelMax->Number() < channel->Number()) { Schedule = Schedules->Next(Schedule); continue; } } if (useChannel == 2 && channelGroup) { cChannelGroup* group = ChannelGroups.GetGroupByName(channelGroup); if (!group || !group->ChannelInGroup(channel)) { Schedule = Schedules->Next(Schedule); continue; } } if (useChannel == 3 && noPayTV) { if (channel->Ca() >= CA_ENCRYPTED_MIN) { Schedule = Schedules->Next(Schedule); continue; } } if (noPayTV) { // no paytv if (channel->Ca() >= CA_ENCRYPTED_MIN) { Schedule = 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 = Schedules->Next(Schedule); } LogFile.Log(3, "found %d event(s) for search timer '%s'", counter, search); } // Give up locks 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) && (action != searchTimerActionInactiveRecord)) { 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) && (action != searchTimerActionInactiveRecord)) { // 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, compareSummaryMatchInPercent)) { 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; const 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) { LOCK_TIMERS_WRITE; for (cTimer *ti = Timers->First(); ti; ti = Timers->Next(ti)) { if (((!bOn && ti->HasFlags(tfActive)) || (bOn && !ti->HasFlags(tfActive))) && TriggeredFromSearchTimerID(ti) == ID) ti->OnOff(); } } void cSearchExt::DeleteAllTimers() { cList DelTimers; LOCK_TIMERS_WRITE; Timers->SetExplicitModify(); 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; LOCK_TIMERS_READ; for (const 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; LOCK_RECORDINGS_READ; for (const 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 < contentsFilter.size(); i += 2) { std::string hexContentID = contentsFilter.substr(i, 2); if (hexContentID.size() != 2) return false; std::istringstream iss(hexContentID); int tmpContentID = 0; if (!(iss >> 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 << std::noshowbase << i; contentsFilter += oss.str(); } } } bool cSearchExt::MatchesContentsFilter(const cEvent* e) { if (!e) return false; // check if each content filter ID is contained in the events descriptors for (unsigned int i = 0; i < contentsFilter.size(); i += 2) { std::string hexContentID = contentsFilter.substr(i, 2); if (hexContentID.size() != 2) return false; std::istringstream iss(hexContentID); int searchContentID = 0; if (!(iss >> 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; } // -- 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) != NULL) { 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 = std::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); }