diff options
| -rw-r--r-- | HISTORY | 4 | ||||
| -rw-r--r-- | vdrtva-2.3.c | 1718 |
2 files changed, 1722 insertions, 0 deletions
@@ -97,3 +97,7 @@ VDR Plugin 'vdrtva' Revision History - Add channel name of first event to links file entries. - Only add item to a series if it is on the same channel as the original (fix for UK "Channel 5" stable using same series CRID on multiple channels). + +2015-02-23: Version 0.3.5 +- Update patch for VDR 2.2.0. + diff --git a/vdrtva-2.3.c b/vdrtva-2.3.c new file mode 100644 index 0000000..6df25e0 --- /dev/null +++ b/vdrtva-2.3.c @@ -0,0 +1,1718 @@ +/* + * vdrtva.c: A plugin for the Video Disk Recorder + * + * See the README file for copyright information and how to reach the author. + * + * $Id$ + */ + +#include <vdr/plugin.h> +#include <libsi/section.h> +#include <libsi/descriptor.h> +#include <stdarg.h> +#include <getopt.h> +#include <pwd.h> +#include "vdrtva.h" + +#define REPORT(a...) void( (tvalog.mailFrom()) ? tvalog.Append(a) : tvasyslog(a) ) + + +cChanDAs ChanDAs; +cEventCRIDs EventCRIDs; +cSuggestCRIDs SuggestCRIDs; +cLinks Links; +cTvaLog tvalog; +char *configDir; + +static const char *VERSION = "0.3.3"; +static const char *DESCRIPTION = "Series Record plugin"; +static const char *MAINMENUENTRY = "Series Links"; + +int collectionperiod; // Time to collect all CRID data (default 10 minutes) +int lifetime; // Lifetime of series link recordings (default 99) +int priority; // Priority of series link recordings (default 99) +int seriesLifetime; // Expiry time of a series link (default 30 days) +int updatetime; // Time to carry out the series link update HHMM (default 03:00) +bool checkCollisions; // Whether to test for collisions (assuming single DVB card) +bool captureComplete; // Flag set if initial CRID capture has completed. +time_t startTime; // Time the plugin was initialised. + +class cPluginvdrTva : public cPlugin { +private: + // Add any member variables or functions you may need here. + time_t nextactiontime; + cTvaFilter *Filter; + cTvaStatusMonitor *statusMonitor; + bool AppendItems(const char* Option); + void UpdateLinksFromTimers(void); + void AddNewEventsToSeries(void); + bool CheckSplitTimers(void); + bool CreateTimerFromEvent(const cEvent *event, char *Path); + void CheckChangedEvents(void); + void CheckTimerClashes(void); + void FindAlternatives(const cEvent *event); + void FindSuggestions(const cEvent *event); + void StartDataCapture(void); + void StopDataCapture(void); + void Update(void); + void Check(bool daily); + void Report(void); + void Expire(void); + void tvasyslog(const char *Fmt, ...); + time_t NextUpdateTime(void); + +public: + cPluginvdrTva(void); + virtual ~cPluginvdrTva(); + virtual const char *Version(void) { return VERSION; } + virtual const char *Description(void) { return tr(DESCRIPTION); } + virtual const char *CommandLineHelp(void); + virtual bool ProcessArgs(int argc, char *argv[]); + virtual bool Initialize(void); + virtual bool Start(void); + virtual void Stop(void); + virtual void Housekeeping(void); + virtual void MainThreadHook(void); + virtual cString Active(void); + virtual time_t WakeupTime(void); + virtual const char *MainMenuEntry(void) { return tr(MAINMENUENTRY); } + virtual cOsdObject *MainMenuAction(void); + virtual cMenuSetupPage *SetupMenu(void); + virtual bool SetupParse(const char *Name, const char *Value); + virtual bool Service(const char *Id, void *Data = NULL); + virtual const char **SVDRPHelpPages(void); + virtual cString SVDRPCommand(const char *Command, const char *Option, int &ReplyCode); + }; + +cPluginvdrTva::cPluginvdrTva(void) +{ + // Initialize any member variables here. + // DON'T DO ANYTHING ELSE THAT MAY HAVE SIDE EFFECTS, REQUIRE GLOBAL + // VDR OBJECTS TO EXIST OR PRODUCE ANY OUTPUT! + configDir = NULL; + Filter = NULL; + seriesLifetime = 30 * SECSINDAY; + priority = 99; + lifetime = 99; + collectionperiod = 10 * 60; + updatetime = 300; + captureComplete = false; + checkCollisions = true; + startTime = time(NULL); +} + +cPluginvdrTva::~cPluginvdrTva() +{ + // Clean up after yourself! +} + +const char *cPluginvdrTva::CommandLineHelp(void) +{ + // Return a string that describes all known command line options. + return " -l n --lifetime=n Lifetime of new timers (default 99)\n" + " -m addr --mailaddr=addr Address to send mail report\n" + " -n --nocheck Do not check for timer collisions\n" + " -p n --priority=n Priority of new timers (default 99)\n" + " -s n --serieslifetime=n Days to remember a series after the last event\n" + " (default 30)\n" + " -u HH:MM --updatetime=HH:MM Time to update series links (default 03:00)\n"; +} + +bool cPluginvdrTva::ProcessArgs(int argc, char *argv[]) +{ + // Implement command line argument processing here if applicable. + static struct option long_options[] = { + { "serieslifetime", required_argument, NULL, 's' }, + { "priority", required_argument, NULL, 'p' }, + { "lifetime", required_argument, NULL, 'l' }, + { "updatetime", required_argument, NULL, 'u' }, + { "mailaddr", required_argument, NULL, 'm' }, + { "nocheck", no_argument, NULL, 'n' }, + { NULL } + }; + + int c, opt; + char *hours, *mins, *strtok_next; + char buf[32]; + while ((c = getopt_long(argc, argv, "l:m:n:p:s:u:", long_options, NULL)) != -1) { + switch (c) { + case 'l': + opt = atoi(optarg); + if (opt > 0) lifetime = opt; + break; + case 'm': + tvalog.setmailTo(optarg); + break; + case 'n': + checkCollisions = false; + break; + case 'p': + opt = atoi(optarg); + if (opt > 0) priority = opt; + break; + case 's': + opt = atoi(optarg); + if (opt > 0) seriesLifetime = opt * SECSINDAY; + break; + case 'u': + strncpy(buf, optarg,sizeof(buf)); + hours = strtok_r(buf, ":", &strtok_next); + mins = strtok_r(NULL, "!", &strtok_next); + updatetime = atoi(hours)*100 + atoi(mins); + break; + default: + return false; + } + } + return true; +} + +bool cPluginvdrTva::Initialize(void) +{ + // Initialize any background activities the plugin shall perform. + return true; +} + +bool cPluginvdrTva::Start(void) +{ + // Start any background activities the plugin shall perform. + configDir = strcpyrealloc(configDir, cPlugin::ConfigDirectory("vdrtva")); + Links.Load(); + statusMonitor = new cTvaStatusMonitor; + if (tvalog.mailTo()) { + struct stat sb; + if (stat("/usr/sbin/sendmail", &sb) == 0) { + char hostname[256]; + if (!gethostname (hostname, sizeof(hostname))) { + char buf[16384]; + struct passwd pwd, *result; + size_t bufsize = sizeof(buf); + int s = getpwuid_r (getuid(), &pwd, buf, bufsize, &result); + if ((s == 0) && (result != NULL)) { + char from[256]; + sprintf(from, "%s@%s", pwd.pw_name, hostname); + tvalog.setmailFrom(from); + isyslog("vdrtva: daily reports will be mailed from %s to %s", from, tvalog.mailTo()); + } + else { + esyslog("vdrtva: unable to establish vdr's user name"); + } + } + else { + esyslog("vdrtva: unable to establish vdr's hostname"); + } + } + else { + esyslog("vdrtva: no mail server found"); + } + } + nextactiontime = time(NULL) + collectionperiod + 1; // wait for CRIDs to be collected + return true; +} + +void cPluginvdrTva::Stop(void) +{ + // Stop any background activities the plugin is performing. + if (Filter) { + delete Filter; + Filter = NULL; + } + tvalog.MailLog(); + if(statusMonitor) delete statusMonitor; + Links.Save(); +} + +void cPluginvdrTva::Housekeeping(void) +{ + // Perform any cleanup or other regular tasks. + static int state = 0; + + if (nextactiontime < time(NULL)) { + statusMonitor->ClearTimerAdded(); // Ignore any timer changes while update is in progress + switch (state) { + case 0: + Expire(); + Update(); + state++; + break; + case 1: + Check(1); + Report(); + nextactiontime = NextUpdateTime(); + state = 0; + tvalog.MailLog(); + break; + } + } + else if (statusMonitor->GetTimerAddedDelta() > 60) { + Update(); // Wait 1 minute for VDR to enter the event data into the new timer. + Check(0); + statusMonitor->ClearTimerAdded(); + } +} + +time_t cPluginvdrTva::NextUpdateTime(void) +{ + struct tm tm_r; + time_t now, then; + char buff[32]; + + now = time(NULL); + localtime_r(&now, &tm_r); + tm_r.tm_sec = 0; + tm_r.tm_hour = updatetime / 100; + tm_r.tm_min = updatetime % 100; + tm_r.tm_mday++; + then = mktime(&tm_r); + if (then < now) then += SECSINDAY; + ctime_r(&then, buff); + isyslog("vdrtva: Next update due at %s", buff); + return then; +} + +void cPluginvdrTva::MainThreadHook(void) +{ + // Perform actions in the context of the main program thread. + // WARNING: Use with great care - see PLUGINS.html! + + static bool running = false; + + if (!running && (time(NULL) - startTime > 5)) { + StartDataCapture(); + running = true; + } + if (!captureComplete && (time(NULL) - startTime > collectionperiod)) { + captureComplete = true; + } +} + +cString cPluginvdrTva::Active(void) +{ + // Return a message string if shutdown should be postponed + return NULL; +} + +time_t cPluginvdrTva::WakeupTime(void) +{ + // Return custom wakeup time for shutdown script + return 0; +} + +cOsdObject *cPluginvdrTva::MainMenuAction(void) +{ + // Perform the action when selected from the main VDR menu. + return new cMenuLinks; +} + +cMenuSetupPage *cPluginvdrTva::SetupMenu(void) +{ + // Return a setup menu in case the plugin supports one. + return new cTvaMenuSetup; +} + +bool cPluginvdrTva::SetupParse(const char *Name, const char *Value) +{ + // Parse your own setup parameters and store their values. + if (!strcasecmp(Name, "CollectionPeriod")) collectionperiod = atoi(Value) * 60; + else if (!strcasecmp(Name, "SeriesLifetime")) seriesLifetime = atoi(Value) * SECSINDAY; + else if (!strcasecmp(Name, "TimerLifetime")) lifetime = atoi(Value); + else if (!strcasecmp(Name, "TimerPriority")) priority = atoi(Value); + else if (!strcasecmp(Name, "UpdateTime")) updatetime = atoi(Value); + else if (!strcasecmp(Name, "CheckCollisions")) checkCollisions = atoi(Value); + else return false; + return true; +} + +bool cPluginvdrTva::Service(const char *Id, void *Data) +{ + // Handle custom service requests from other plugins + return false; +} + +const char **cPluginvdrTva::SVDRPHelpPages(void) +{ + // Return help text for SVDRP commands this plugin implements + static const char *HelpPages[] = { + "DELL <sCrid>\n" + " Delete series link by series CRID", + "LLOG\n" + " Print the action log.", + "LSTL\n" + " Print the Links list.", + "LSTS\n" + " Print the suggested events list", + "LSTT\n" + " Print the list of timers with suggestions for each event", + "LSTY\n" + " Print the Event list including CRIDs.", + "LSTZ\n" + " Print the channel list with Default Authority.", + "STOP\n" + " Stop Event data capture (retaining data).", + "STRT\n" + " Start Event data capture (erasing any existing data)", + "UPDT\n" + " Update timers and links (series link functionality)", + NULL + }; + return HelpPages; +} + +cString cPluginvdrTva::SVDRPCommand(const char *Command, const char *Option, int &ReplyCode) +{ + // Process SVDRP commands this plugin implements + cTvaLog reply; + isyslog ("vdrtva: processing command %s", Command); + if (strcasecmp(Command, "DELL") == 0) { + if (!captureComplete) return cString::sprintf("Data capture still in progress"); + if (Links.DeleteItem(Option)) { + return cString::sprintf("Series %s deleted", Option); + } + else return cString::sprintf("Series %s not found in links file", Option); + } + else if (strcasecmp(Command, "LLOG") == 0) { + ReplyCode = 250; + if (tvalog.Length() > 0) return cString(tvalog.Buffer()); + else return cString::sprintf("Nothing in the buffer!"); + } + else if (strcasecmp(Command, "LSTL") == 0) { + if (Links.MaxNumber() >=1) { + ReplyCode = 250; + for (cLinkItem *linkItem = Links.First(); linkItem; linkItem = Links.Next(linkItem)) { + reply.Append("%s;%d;%s;%s;%s;%s\n", linkItem->sCRID(), linkItem->ModTime(), linkItem->iCRIDs(), linkItem->Path(), linkItem->Title(), linkItem->channelName()); + } + } + if (reply.Length() > 0) return cString(reply.Buffer()); + else return cString::sprintf("Nothing in the buffer!"); + } + else if (strcasecmp(Command, "LSTS") == 0) { + if (!captureComplete) return cString::sprintf("Data capture still in progress"); + if (SuggestCRIDs.MaxNumber() >= 1) { + ReplyCode = 250; + cSuggestCRID *suggest = SuggestCRIDs.First(); + while (suggest) { + cSuggestCRID *next = SuggestCRIDs.Next(suggest); + cChanDA *chanDA = ChanDAs.GetByChannelID(suggest->Cid()); + if(chanDA) { + reply.Append("%s%s %s%s\n", chanDA->DA(), suggest->iCRID(), chanDA->DA(), suggest->gCRID()); + } + suggest = next; + } + if (reply.Length() > 0) return cString(reply.Buffer()); + else return cString::sprintf("Nothing in the buffer!"); + } + else + return cString::sprintf("No suggested events defined"); + } + else if (strcasecmp(Command, "LSTT") == 0) { + LOCK_TIMERS_READ; + if (Timers->Count() == 0) return cString::sprintf("No timers defined"); + Report(); + return cString::sprintf("Report generated"); + } + else if (strcasecmp(Command, "LSTY") == 0) { + if (EventCRIDs.MaxNumber() >= 1) { + ReplyCode = 250; + for (cEventCRID *eventCRID = EventCRIDs.First(); eventCRID; eventCRID = EventCRIDs.Next(eventCRID)) { + cChanDA *chanDA = ChanDAs.GetByChannelID(eventCRID->Cid()); + if(chanDA) { + reply.Append("%d %d %s%s %s%s\n", chanDA->Cid(), eventCRID->Eid(), chanDA->DA(), eventCRID->iCRID(), chanDA->DA(), eventCRID->sCRID()); + } + } + if (reply.Length() > 0) return cString(reply.Buffer()); + else return cString::sprintf("Nothing in the buffer!"); + } + else + return cString::sprintf("No events defined"); + } + else if (strcasecmp(Command, "LSTZ") == 0) { + if (ChanDAs.MaxNumber() >= 1) { + ReplyCode = 250; + for (cChanDA *chanDA = ChanDAs.First(); chanDA; chanDA = ChanDAs.Next(chanDA)) { + reply.Append("%d %s\n", chanDA->Cid(), chanDA->DA()); + } + if (reply.Length() > 0) return cString(reply.Buffer()); + else return cString::sprintf("Nothing in the buffer!"); + } + else + return cString::sprintf("No channels defined"); + } + else if (strcasecmp(Command, "STRT") == 0) { + if (!Filter) { + StartDataCapture(); + return cString::sprintf("Data capture started"); + } + else { + ReplyCode = 999; + return cString::sprintf("Double start attempted"); + } + } + else if (strcasecmp(Command, "STOP") == 0) { + if (Filter) { + StopDataCapture(); + return cString::sprintf("Data capture stopped"); + } + else { + ReplyCode = 999; + return cString::sprintf("Double stop attempted"); + } + } + else if (strcasecmp(Command, "UPDT") == 0) { + if (captureComplete) { + Update(); + Check(0); + return cString::sprintf("Update completed"); + } + else { + ReplyCode = 999; + return cString::sprintf("Data capture in progress"); + } + } + return NULL; +} + +void cPluginvdrTva::StartDataCapture() +{ + if (!Filter) { + Filter = new cTvaFilter(); + cDevice::ActualDevice()->AttachFilter(Filter); + isyslog("vdrtva: Data capture started"); + } +} + +void cPluginvdrTva::StopDataCapture() +{ + if (Filter) { + delete Filter; + Filter = NULL; + isyslog("vdrtva: Data capture stopped"); + } +} + +void cPluginvdrTva::Expire() +{ + EventCRIDs.Expire(); + SuggestCRIDs.DeDup(); + SuggestCRIDs.Expire(); + Links.Expire(); + Links.Save(); +} + +void cPluginvdrTva::Update() +{ + UpdateLinksFromTimers(); + AddNewEventsToSeries(); + Links.Save(); + isyslog("vdrtva: Updates complete"); +} + +void cPluginvdrTva::Check(bool daily) +{ + if (daily) { + REPORT(" \nDaily Timer Check\n-----------------\n "); + } + CheckChangedEvents(); + if (checkCollisions) { + CheckTimerClashes(); + } + CheckSplitTimers(); + isyslog("vdrtva: Checks complete"); +} + +void cPluginvdrTva::Report() +{ + LOCK_TIMERS_READ; + if ((Timers->Count() == 0) || (!captureComplete)) return; + REPORT(" \nTimers and Suggestions\n----------------------\n "); + cSortedTimers SortedTimers(Timers); + for (int i = 0; i < SortedTimers.Size(); i++) { + if (const cTimer *ti = SortedTimers[i]) { + const cEvent *event = ti->Event(); + if (event && ti->HasFlags(tfActive)) { + REPORT("'%s' (%s %s)", event->Title(), ti->Channel()->Name(), *DayDateTime(event->StartTime())); + FindSuggestions(event); + } + } + } +} + +// Check that all timers are part of series links and update the links. + +void cPluginvdrTva::UpdateLinksFromTimers() +{ + LOCK_TIMERS_READ; + LOCK_CHANNELS_READ; + if ((Timers->Count() == 0) || (!captureComplete)) return; + for (const cTimer *ti = Timers->First(); ti; ti = Timers->Next(ti)) { +// find the event for this timer + const cEvent *event = ti->Event(); + if (event && ti->HasFlags(tfActive) && (ti->WeekDays() == 0)) { + const cChannel *channel = Channels->GetByChannelID(event->ChannelID()); +// find the sCRID and iCRID for the event + cChanDA *chanda = ChanDAs.GetByChannelID(channel->Number()); + cEventCRID *eventcrid = EventCRIDs.GetByID(channel->Number(), event->EventID()); + if (eventcrid && chanda) { + cString scrid = cString::sprintf("%s%s", chanda->DA(),eventcrid->sCRID()); + cString icrid = cString::sprintf("%s%s", chanda->DA(),eventcrid->iCRID()); +// scan the links table for the sCRID +// if found, check if the iCRID is present, if not add it +// else create a new links entry + char *path = strcpyrealloc(NULL, ti->File()); + if (char *p = strrchr(path, '~')) { + *p = '\0'; + p++; + Links.AddSeriesLink(scrid, event->StartTime(), icrid, path, p, channel->Name()); + } + else Links.AddSeriesLink(scrid, event->StartTime(), icrid, NULL, path, channel->Name()); + free (path); + } + } + } +} + +void cPluginvdrTva::AddNewEventsToSeries() +{ + if (Links.MaxNumber() < 1) return; +// Foreach CRID + for (cEventCRID *eventCRID = EventCRIDs.First(); eventCRID; eventCRID = EventCRIDs.Next(eventCRID)) { + cChanDA *chanDA = ChanDAs.GetByChannelID(eventCRID->Cid()); + if (chanDA) { +// Do we have a series link for this sCRID? + cString scrid = cString::sprintf("%s%s", chanDA->DA(),eventCRID->sCRID()); + cLinkItem *Item = Links.getLinkItem(scrid); + if (Item != NULL) { +// Is the event already being recorded, possibly as part of a different series? + cString icrid = cString::sprintf("%s%s", chanDA->DA(),eventCRID->iCRID()); + if (Links.isEventNeeded(icrid)) { +// Is the event on the same channel as the first event of the series? If so create a new timer. + LOCK_CHANNELS_READ; + LOCK_SCHEDULES_READ; + const cChannel *channel = Channels->GetByNumber(eventCRID->Cid()); + const cSchedule *schedule = Schedules->GetSchedule(channel); + if (schedule && (!Item->channelName() || !strcmp(channel->Name(), Item->channelName()))) { + const cEvent *event = schedule->GetEvent(eventCRID->Eid()); + if (CreateTimerFromEvent(event, Item->Path())) { + Links.AddSeriesLink(scrid, event->StartTime(), icrid, NULL, NULL, NULL); + } + } + } + } + } + } +} + +// Check timers to see if the event they were set to record is still in the EPG. +// This won't work if VPS is not used and the start time is padded by a custom amount. + +void cPluginvdrTva::CheckChangedEvents() +{ + LOCK_TIMERS_READ; + LOCK_SCHEDULES_READ; + if (Timers->Count() == 0) return; + for (const cTimer *ti = Timers->First(); ti; ti = Timers->Next(ti)) { + const cChannel *channel = ti->Channel(); + const cSchedule *schedule = Schedules->GetSchedule(channel); + if (schedule && ti->HasFlags(tfActive)) { + time_t start_time = ti->StartTime(); + if (!ti->HasFlags(tfVps)) { + start_time += Setup.MarginStart * 60; + } + const cEvent *event = schedule->GetEvent(0, start_time); + const char *file = strrchr(ti->File(), '~'); + if (!file) file = ti->File(); + else file++; + if (!event) REPORT("Event for timer '%s' at %s seems to no longer exist", file, *DayDateTime(ti->StartTime())); + else if (strcmp(file, event->Title())) { + REPORT("Changed timer event at %s: %s <=> %s", *DayDateTime(ti->StartTime()), file, event->Title()); + } + } + } +} + +// Check for timer clashes - overlapping timers which are not on the same transponder. +// FIXME How to deal with multiple input devices?? + +void cPluginvdrTva::CheckTimerClashes(void) +{ + LOCK_TIMERS_READ; + if (Timers->Count() < 2) return; + for (int i = 1; i < Timers->Count(); i++) { + const cTimer *timer1 = Timers->Get(i); + if (timer1 && timer1->HasFlags(tfActive)) { + for (int j = 0; j < i; j++) { + const cTimer *timer2 = Timers->Get(j); + if (timer2 && timer2->HasFlags(tfActive)) { + if((timer1->StartTime() >= timer2->StartTime() && timer1->StartTime() < timer2->StopTime()) + ||(timer2->StartTime() >= timer1->StartTime() && timer2->StartTime() < timer1->StopTime())) { + const cChannel *channel1 = timer1->Channel(); + const cChannel *channel2 = timer2->Channel(); + if (channel1->Transponder() != channel2->Transponder()) { + REPORT("Timer collision at %s. %s <=> %s", *DayDateTime(timer1->StartTime()), timer1->File(), timer2->File()); + FindAlternatives(timer1->Event()); + FindAlternatives(timer2->Event()); + } + } + } + } + } + } +} + +// Find alternative broadcasts for an event, ie events in the EIT having the same CRID. +// Check whether adding a timer for the alternative would create a clash with any other timer. + +void cPluginvdrTva::FindAlternatives(const cEvent *event) +{ + if (!event) { + dsyslog("vdrtva: FindAlternatives() called without Event!"); + return; + } + LOCK_TIMERS_READ; + LOCK_CHANNELS_READ; + LOCK_SCHEDULES_READ; + const cChannel *channel = Channels->GetByChannelID(event->ChannelID()); + cChanDA *chanda = ChanDAs.GetByChannelID(channel->Number()); + cEventCRID *eventcrid = EventCRIDs.GetByID(channel->Number(), event->EventID()); + if (!eventcrid || !chanda) { + REPORT("Cannot find alternatives for '%s' - not part of a series", event->Title()); + return; + } + bool found = false; + for (cEventCRID *eventcrid2 = EventCRIDs.First(); eventcrid2; eventcrid2 = EventCRIDs.Next(eventcrid2)) { + if ((strcmp(eventcrid->iCRID(), eventcrid2->iCRID()) == 0) && (event->EventID() != eventcrid2->Eid())) { + cChanDA *chanda2 = ChanDAs.GetByChannelID(eventcrid2->Cid()); + if (strcmp(chanda->DA(), chanda2->DA()) == 0) { + const cChannel *channel2 = Channels->GetByNumber(eventcrid2->Cid()); + const cSchedule *schedule = Schedules->GetSchedule(channel2); + if (schedule) { + const cEvent *event2 = schedule->GetEvent(eventcrid2->Eid(), 0); + if (event2) { + if (!found) { + REPORT("Alternatives for '%s':", event->Title()); + found = true; + } + bool clash = false; + for (const cTimer *ti = Timers->First(); ti; ti = Timers->Next(ti)) { + if((ti->StartTime() >= event2->StartTime() && ti->StartTime() < event2->EndTime()) + ||(event2->StartTime() >= ti->StartTime() && event2->StartTime() < ti->StopTime())) { + if (ti->Channel()->Transponder() != channel2->Transponder()) { + const char *file = strrchr(ti->File(), '~'); + if (!file) file = ti->File(); + else file++; + REPORT(" %s %s (clash with timer '%s')", channel2->Name(), *DayDateTime(event2->StartTime()), file); + clash = true; + } + } + } + if (!clash) { + REPORT(" %s %s", channel2->Name(), *DayDateTime(event2->StartTime())); + } + } + } + } + } + } + if (!found) REPORT("No alternatives for '%s'", event->Title()); +} + +// Check that, if any split events (eg a long programme with a news break in the middle) +// are being recorded, that timers are set for all of the parts. +// FIXME This may not work if the programme is being repeated. Inefficient algorithm. + +bool cPluginvdrTva::CheckSplitTimers(void) +{ + LOCK_TIMERS_READ; + LOCK_CHANNELS_READ; + if (Timers->Count() == 0) return false; + for (const cTimer *ti = Timers->First(); ti; ti = Timers->Next(ti)) { + const cEvent *event = ti->Event(); + if (event && ti->HasFlags(tfActive)) { + const cChannel *channel = Channels->GetByChannelID(event->ChannelID()); + cChanDA *chanda = ChanDAs.GetByChannelID(channel->Number()); + cEventCRID *eventcrid = EventCRIDs.GetByID(channel->Number(), event->EventID()); + if (eventcrid && chanda && strchr(eventcrid->iCRID(), '#')) { +// char crid[Utf8BufSize(256)], *next; +// strcpy(crid, eventcrid->iCRID()); +// char *prefix = strtok_r(crid, "#", &next); +// char *suffix = strtok_r(NULL, "#", &next); + REPORT("Timer for split event '%s' found - check all parts are being recorded!", event->Title()); + } + } + } + return false; +} + +// Create a timer from an event, setting VPS parameter explicitly. + +bool cPluginvdrTva::CreateTimerFromEvent(const cEvent *event, char *Path) { + if (!event) { + dsyslog("vdrtva: CreateTimerFromEvent() called without Event!"); + return false; + } + LOCK_TIMERS_WRITE; + LOCK_CHANNELS_READ; + struct tm tm_r; + char startbuff[64], endbuff[64], etitle[256]; + int flags = tfActive; + const cChannel *channel = Channels->GetByChannelID(event->ChannelID()); + time_t starttime = event->StartTime(); + time_t endtime = event->EndTime(); + if (!Setup.UseVps) { + starttime -= Setup.MarginStart * 60; + endtime += Setup.MarginStop * 60; + } + else flags |= tfVps; + localtime_r(&starttime, &tm_r); + strftime(startbuff, sizeof(startbuff), "%Y-%m-%d:%H%M", &tm_r); + localtime_r(&endtime, &tm_r); + strftime(endbuff, sizeof(endbuff), "%H%M", &tm_r); + strn0cpy (etitle, event->Title(), sizeof(etitle)); + strreplace(etitle, ':', '|'); + cString timercmd; + if (Path) { + timercmd = cString::sprintf("%u:%d:%s:%s:%d:%d:%s~%s:\n", flags, channel->Number(), startbuff, endbuff, priority, lifetime, Path, etitle); + } + else { + timercmd = cString::sprintf("%u:%d:%s:%s:%d:%d:%s:\n", flags, channel->Number(), startbuff, endbuff, priority, lifetime, etitle); + } + cTimer *timer = new cTimer; + if (timer->Parse(timercmd)) { + const cTimer *t = Timers->GetTimer(timer); + if (!t) { + timer->SetEvent(event); + Timers->Add(timer); + Timers->SetModified(); + REPORT("Timer created for '%s' on %s, %s %04d-%04d", etitle, channel->Name(), *DateString(starttime), timer->Start(), timer->Stop()); + return true; + } + isyslog("vdrtva: Duplicate timer creation attempted for %s on %s", *timer->ToDescr(), *DateString(timer->StartTime())); + } + return false; +} + +// Find 'suggestions' for an event + +void cPluginvdrTva::FindSuggestions(const cEvent *event) +{ + bool found = false; + LOCK_CHANNELS_READ; + LOCK_SCHEDULES_READ; + const cChannel *channel = Channels->GetByChannelID(event->ChannelID()); + cChanDA *chanda = ChanDAs.GetByChannelID(channel->Number()); + cEventCRID *eventcrid = EventCRIDs.GetByID(channel->Number(), event->EventID()); + if (eventcrid && chanda) { + for (cSuggestCRID *suggestcrid = SuggestCRIDs.First(); suggestcrid; suggestcrid = SuggestCRIDs.Next(suggestcrid)) { + if((channel->Number() == suggestcrid->Cid()) && (!strcmp(suggestcrid->iCRID(), eventcrid->iCRID()))) { + for (cEventCRID *ecrid2 = EventCRIDs.First(); ecrid2; ecrid2 = EventCRIDs.Next(ecrid2)) { + if (!strcmp(suggestcrid->gCRID(), ecrid2->iCRID())) { + cChanDA *chanda2 = ChanDAs.GetByChannelID(ecrid2->Cid()); + if (!strcmp(chanda->DA(), chanda2->DA())) { + const cChannel *channel2 = Channels->GetByNumber(ecrid2->Cid()); + const cSchedule *schedule = Schedules->GetSchedule(channel2); + if (schedule) { + const cEvent *event2 = schedule->GetEvent(ecrid2->Eid(), 0); + if (!found) { + REPORT(" Suggestions for this event:"); + found = true; + } + REPORT(" '%s' (%s, %s)", event2->Title(), channel2->Name(), *DayDateTime(event2->StartTime())); + } + } + } + } + } + } + } +} + +// Report actions to syslog if we don't want an email. + +void cPluginvdrTva::tvasyslog(const char *Fmt, ...) { + + va_list ap; + int size = 4096; + char *buff = (char *) malloc(sizeof(char) * size); + + while (buff) { + va_start(ap, Fmt); + int n = vsnprintf(buff, size, Fmt, ap); + va_end(ap); + if (n < size) { + char *save, *b = buff; + while (b = strtok_r(b, "\n", &save)) { + isyslog("vdrtva: %s", b); + b = NULL; + } + free(buff); + return; + } +// overflow: realloc and try again + size *= 2; + char *tmp = (char *) realloc(buff, sizeof(char) * size); + if (!tmp) free(buff); + buff = tmp; + } +} + + + +/* + cTvaStatusMonitor - callback for timer changes. +*/ + +cTvaStatusMonitor::cTvaStatusMonitor(void) +{ + timeradded = 0; + lasttimer = NULL; +} + +void cTvaStatusMonitor::TimerChange(const cTimer *Timer, eTimerChange Change) +{ + if (Change == tcAdd) { + timeradded = time(NULL); + lasttimer = Timer; + } +} + +int cTvaStatusMonitor::GetTimerAddedDelta(void) +{ + if (timeradded) { + return (time(NULL) - timeradded); + } + return 0; +} + +void cTvaStatusMonitor::ClearTimerAdded(void) +{ + timeradded = 0; + return; +} + + +/* + cTvaMenuSetup - setup menu function. +*/ + +cTvaMenuSetup::cTvaMenuSetup(void) +{ + newcollectionperiod = collectionperiod / 60; + newlifetime = lifetime; + newpriority = priority; + newseriesLifetime = seriesLifetime / SECSINDAY; + newupdatetime = updatetime; + newcheckcollisions = checkCollisions; + Add(new cMenuEditIntItem(tr("Collection period (min)"), &newcollectionperiod, 1, 99)); + Add(new cMenuEditIntItem(tr("Series link lifetime (days)"), &newseriesLifetime, 1, 366)); + Add(new cMenuEditIntItem(tr("New timer lifetime"), &newlifetime, 0, 99)); + Add(new cMenuEditIntItem(tr("New timer priority"), &newpriority, 0, 99)); + Add(new cMenuEditTimeItem(tr("Update Time (HH:MM)"), &newupdatetime)); + Add(new cMenuEditBoolItem(tr("Check collisions"), &newcheckcollisions)); +} + +void cTvaMenuSetup::Store(void) +{ + SetupStore("CollectionPeriod", newcollectionperiod); collectionperiod = newcollectionperiod * 60; + SetupStore("SeriesLifetime", newseriesLifetime); seriesLifetime = newseriesLifetime * SECSINDAY; + SetupStore("TimerLifetime", newlifetime); lifetime = newlifetime; + SetupStore("TimerPriority", newpriority); priority = newpriority; + SetupStore("UpdateTime", newupdatetime); updatetime = newupdatetime; + SetupStore("CheckCollisions", newcheckcollisions); checkCollisions = newcheckcollisions; +} + + +/* + cTvaLog - logging class +*/ + +cTvaLog::cTvaLog(void) { + buffer = mailfrom = mailto = NULL; +} + +cTvaLog::~cTvaLog(void) { + if (buffer) free(buffer); + if (mailfrom) free(mailfrom); + if (mailto) free(mailto); +} + +// Append an entry to the log. Ensure the entry is CR-terminated. + +void cTvaLog::Append(const char *Fmt, ...) +{ + va_list ap; + + if (!buffer) { + length = 0; + size = 4096; + buffer = (char *) malloc(sizeof(char) * size); + } + while (buffer) { + va_start(ap, Fmt); + int n = vsnprintf(buffer + length, size - length, Fmt, ap); + va_end(ap); + if (n < size - length - 1) { + length += n; + if (*(buffer+length-1) != '\n') { + *(buffer+length) = '\n'; + length++; + *(buffer+length) = '\0'; + } + return; + } +// overflow: realloc and try again + size *= 2; + char *tmp = (char *) realloc(buffer, sizeof(char) * size); + if (!tmp) free(buffer); + buffer = tmp; + } + return; +} + +int cTvaLog::Length(void) { + if (!buffer) return 0; + return length; +} + +void cTvaLog::setmailTo(char *opt) { + mailto = strcpyrealloc(mailto, opt); +} + +void cTvaLog::setmailFrom(char *opt) { + mailfrom = strcpyrealloc(mailfrom, opt); +} + +// Mail out the daily report. + +void cTvaLog::MailLog(void) { +FILE* mail; +char mailcmd[256]; + + if (length == 0) return; + + snprintf(mailcmd, sizeof(mailcmd), "/usr/sbin/sendmail -i -oem %s", mailto); + if (!(mail = popen(mailcmd, "w"))) { + esyslog("vdrtva: cannot open sendmail"); + return; + } + fprintf(mail, "From: %s\n", mailfrom); + fprintf(mail, "To: %s\n", mailto); + fprintf(mail, "Subject: vdrTva report\n"); + fprintf(mail, "Content-Type: text/plain; charset=ISO-8859-1\n\n"); + fprintf(mail, "Activity since last report\n--------------------------\n \n"); + fputs(buffer, mail); + pclose(mail); + Clear(); +} + + +/* + cTvaFilter - capture the CRID data from EIT. +*/ + +cTvaFilter::cTvaFilter(void) +{ + Set(0x11, 0x42); // SDT (Actual) + Set(0x11, 0x46); // SDT (Other) + Set(0x12, 0x40, 0xC0); // event info, actual(0x4E)/other(0x4F) TS, present/following + // event info, actual TS, schedule(0x50)/schedule for future days(0x5X) + // event info, other TS, schedule(0x60)/schedule for future days(0x6X) +} + +void cTvaFilter::Process(u_short Pid, u_char Tid, const u_char *Data, int Length) +{ + // do something with the data here + LOCK_CHANNELS_READ; + switch (Pid) { + case 0x11: { + sectionSyncer.Reset(); + SI::SDT sdt(Data, false); + if (!sdt.CheckCRCAndParse()) { + return; + } + if (!sectionSyncer.Sync(sdt.getVersionNumber(), sdt.getSectionNumber(), sdt.getLastSectionNumber())) { + return; + } + SI::SDT::Service SiSdtService; + for (SI::Loop::Iterator it; sdt.serviceLoop.getNext(SiSdtService, it); ) { + const cChannel *chan = Channels->GetByChannelID(tChannelID(Source(),sdt.getOriginalNetworkId(),sdt.getTransportStreamId(),SiSdtService.getServiceId())); + if (chan) { + cChanDA *chanDA = ChanDAs.GetByChannelID(chan->Number()); + if (!chanDA) { + SI::Descriptor *d; + for (SI::Loop::Iterator it2; (d = SiSdtService.serviceDescriptors.getNext(it2)); ) { + switch (d->getDescriptorTag()) { + case SI::DefaultAuthorityDescriptorTag: { + SI::DefaultAuthorityDescriptor *da = (SI::DefaultAuthorityDescriptor *)d; + char DaBuf[Utf8BufSize(1024)]; + da->DefaultAuthority.getText(DaBuf, sizeof(DaBuf)); + chanDA = ChanDAs.NewChanDA(chan->Number(), DaBuf); + } + break; + default: ; + } + delete d; + } + } + } + } + } + case 0x12: { + if (Tid >= 0x4E && Tid <= 0x6F) { +// sectionSyncer.Reset(); + SI::EIT eit(Data, false); + if (!eit.CheckCRCAndParse()) { + return; + } + + const cChannel *chan = Channels->GetByChannelID(tChannelID(Source(),eit.getOriginalNetworkId(),eit.getTransportStreamId(),eit.getServiceId())); + if (!chan) { + return; + } + SI::EIT::Event SiEitEvent; + for (SI::Loop::Iterator it; eit.eventLoop.getNext(SiEitEvent, it); ) { + cEventCRID *eventCRID = EventCRIDs.GetByID(chan->Number(), SiEitEvent.getEventId()); + if (!eventCRID) { + SI::Descriptor *d; + char iCRIDBuf[Utf8BufSize(256)] = {'\0'}, sCRIDBuf[Utf8BufSize(256)] = {'\0'}, gCRIDBuf[Utf8BufSize(256)] = {'\0'}; + for (SI::Loop::Iterator it2; (d = SiEitEvent.eventDescriptors.getNext(it2)); ) { + switch (d->getDescriptorTag()) { + case SI::ContentIdentifierDescriptorTag: { + SI::ContentIdentifierDescriptor *cd = (SI::ContentIdentifierDescriptor *)d; + SI::ContentIdentifierDescriptor::Identifier cde; + for (SI::Loop::Iterator ite; (cd->identifierLoop.getNext(cde,ite)); ) { + if (cde.getCridLocation() == 0) { + switch (cde.getCridType()) { + case 0x01: // ETSI 102 323 code + case 0x31: // UK Freeview private code + cde.identifier.getText(iCRIDBuf, sizeof(iCRIDBuf)); + break; + case 0x02: // ETSI 102 323 code + case 0x32: // UK Freeview private code + cde.identifier.getText(sCRIDBuf, sizeof(sCRIDBuf)); + break; + // ETSI 102 323 defines CRID type 0x03, which describes 'related' or 'suggested' events. + // Freeview broadcasts these as CRID type 0x33. + // There can be more than one type 0x33 descriptor per event (each with one CRID). + case 0x03: + case 0x33: + cde.identifier.getText(gCRIDBuf, sizeof(gCRIDBuf)); // FIXME Rashly assuming that 0x31 & 0x32 CRIDs will always precede a 0x33 CRID. + if (iCRIDBuf[0] && sCRIDBuf[0]) SuggestCRIDs.NewSuggestCRID(chan->Number(), iCRIDBuf, gCRIDBuf); + } + } + else { + dsyslog ("vdrtva: Incorrect CRID Loc %x", cde.getCridLocation()); + } + } + } + break; + default: ; + } + delete d; + } + if (iCRIDBuf[0] && sCRIDBuf[0]) { // Only log events which are part of a series. + eventCRID = EventCRIDs.NewEventCRID(chan->Number(), SiEitEvent.getEventId(), iCRIDBuf, sCRIDBuf); + } + } + } + } + } + break; + } +} + + +/* + cChanDA - Default Authority for a channel. +*/ + +cChanDA::cChanDA(int Cid, char *DA) +{ + cid = Cid; + if (startswith(DA, "crid://")) defaultAuthority = strcpyrealloc(NULL, &DA[7]); + else defaultAuthority = strcpyrealloc(NULL, DA); +} + +cChanDA::~cChanDA(void) +{ + free(defaultAuthority); +} + +/* + cChanDAs - in-memory list of channels and Default Authorities. +*/ + +cChanDAs::cChanDAs(void) +{ + maxNumber = 0; +} + +cChanDAs::~cChanDAs(void) +{ + chanDAHash.Clear(); +} + +cChanDA *cChanDAs::GetByChannelID(int cid) +{ + cList<cHashObject> *list = chanDAHash.GetList(cid); + if (list) { + for (cHashObject *hobj = list->First(); hobj; hobj = list->Next(hobj)) { + cChanDA *chanDA = (cChanDA *)hobj->Object(); + if (chanDA->Cid() == cid) + return chanDA; + } + } + return NULL; +} + +cChanDA *cChanDAs::NewChanDA(int Cid, char *DA) +{ + cChanDA *NewChanDA = new cChanDA(Cid, DA); + Add(NewChanDA); + chanDAHash.Add(NewChanDA, Cid); + maxNumber++; + return NewChanDA; +} + + +/* + cEventCRID - CRIDs for an event. +*/ + +cEventCRID::cEventCRID(int Cid, tEventID Eid, char *iCRID, char *sCRID) +{ + eid = Eid; + cid = Cid; + iCrid = strcpyrealloc(NULL, iCRID); + sCrid = strcpyrealloc(NULL, sCRID); +} + +cEventCRID::~cEventCRID(void) +{ + free (iCrid); + free (sCrid); +} + + +/* + cEventCRIDs - in-memory list of events and CRIDs. +*/ + +cEventCRIDs::cEventCRIDs(void) +{ + maxNumber = 0; +} + +cEventCRIDs::~cEventCRIDs(void) +{ + EventCRIDHash.Clear(); +} + +cEventCRID *cEventCRIDs::GetByID(int Cid, tEventID Eid) +{ + cList<cHashObject> *list = EventCRIDHash.GetList(Cid*33000 + Eid); + if (list) { + for (cHashObject *hobj = list->First(); hobj; hobj = list->Next(hobj)) { + cEventCRID *EventCRID = (cEventCRID *)hobj->Object(); + if ((EventCRID->Eid() == Eid) && (EventCRID->Cid() == Cid)) + return EventCRID; + } + } + return NULL; +} + +cEventCRID *cEventCRIDs::NewEventCRID(int Cid, tEventID Eid, char *iCRID, char *sCRID) +{ + cEventCRID *NewEventCRID = new cEventCRID(Cid, Eid, iCRID, sCRID); + Add(NewEventCRID); + EventCRIDHash.Add(NewEventCRID, Eid + Cid*33000); + maxNumber++; + return NewEventCRID; +} + +void cEventCRIDs::Expire(void) +{ + int i = 0; + LOCK_CHANNELS_READ; + LOCK_SCHEDULES_READ; + cEventCRID *crid = First(); + while (crid) { + cEventCRID *next = Next(crid); + const cChannel *channel = Channels->GetByNumber(crid->Cid()); + const cSchedule *schedule = Schedules->GetSchedule(channel); + if (schedule) { + const cEvent *event = schedule->GetEvent(crid->Eid(), 0); + if (!event) { + Del(crid); + maxNumber--; + i++; + } + } + crid = next; + } + dsyslog("vdrtva: %d expired CRIDs removed", i); +} + + +/* + cSuggestCRID - CRIDs of suggested items for an event. +*/ + +cSuggestCRID::cSuggestCRID(int Cid, char *iCRID, char *gCRID) +{ + iCrid = strcpyrealloc(NULL, iCRID); + gCrid = strcpyrealloc(NULL, gCRID); + cid = Cid; +} + +cSuggestCRID::~cSuggestCRID(void) +{ + free (iCrid); + free (gCrid); +} + +int cSuggestCRID::Compare(const cListObject &ListObject) const +{ + cSuggestCRID *s = (cSuggestCRID *) &ListObject; + if (int r = cid - s->Cid()) return r; + if (int r = strcmp(iCrid, s->iCRID())) return r; + if (int r = strcmp(gCrid, s->gCRID())) return r; + return 0; +} + + +/* + cSuggestCRIDs - in-memory list of suggested events +*/ + +cSuggestCRIDs::cSuggestCRIDs(void) +{ + maxNumber = 0; +} + +cSuggestCRID *cSuggestCRIDs::NewSuggestCRID(int cid, char *icrid, char *gcrid) +{ + cSuggestCRID *NewSuggestCRID = new cSuggestCRID(cid, icrid, gcrid); + Add(NewSuggestCRID); + maxNumber++; + return NewSuggestCRID; +} + +void cSuggestCRIDs::DeDup(void) { + if (maxNumber < 2) return; + int i = 0; + Sort(); + cSuggestCRID *suggest = First(); + while (suggest) { + cSuggestCRID *next = Next(suggest); + if (next && !strcmp(next->iCRID(), suggest->iCRID()) && !strcmp(next->gCRID(), suggest->gCRID())) { + Del(suggest); + maxNumber--; + i++; + } + suggest = next; + } + dsyslog("vdrtva: %d duplicate suggestions removed", i); +} + +void cSuggestCRIDs::Expire(void) { + if (maxNumber == 0) return; + int i = 0; + cSuggestCRID *suggest = First(); + while (suggest) { + cSuggestCRID *next = Next(suggest); + bool found = false; + for (cEventCRID *crid = EventCRIDs.First(); crid; crid = EventCRIDs.Next(crid)) { + if (!strcmp(suggest->iCRID(), crid->iCRID())) { + found = true; + break; + } + } + if (!found) { + Del(suggest); + maxNumber--; + i++; + } + suggest = next; + } + dsyslog("vdrtva: %d expired suggestions removed", i); +} + + +/* + cLinkItem - Entry from the links file +*/ + +cLinkItem::cLinkItem(const char *sCRID, time_t ModTime, const char *iCRIDs, const char *Path, const char *Title, const char *channelName) +{ + sCrid = strcpyrealloc(NULL, sCRID); + modtime = ModTime; + iCrids = strcpyrealloc(NULL, iCRIDs); + path = strcpyrealloc(NULL, Path); + title = strcpyrealloc(NULL, Title); + channelname = strcpyrealloc(NULL, channelName); +} + +cLinkItem::~cLinkItem(void) +{ + free(sCrid); + free(iCrids); + free(path); + free(title); + free(channelname); +} + +void cLinkItem::SetModtime(time_t ModTime) +{ + modtime = ModTime; + Links.SetUpdated(); +} + +void cLinkItem::SetIcrids(const char *icrids) +{ + iCrids = strcpyrealloc(iCrids, icrids); + Links.SetUpdated(); +} + +/* + cLinks - list of cLinkItem entities +*/ + +cLinks::cLinks(void) +{ + maxNumber = 0; + dirty = false; +} + +cLinkItem *cLinks::NewLinkItem(const char *sCRID, time_t ModTime, const char *iCRIDs, const char *path, const char *title, const char *channelname) +{ + cLinkItem *NewLinkItem = new cLinkItem(sCRID, ModTime, iCRIDs, path, title, channelname); + Add(NewLinkItem); + maxNumber++; + dirty = true; + return NewLinkItem; +} + +void cLinks::Load() +{ + cString curlinks = AddDirectory(configDir, "links.data"); + FILE *f = fopen(curlinks, "r"); + if (f) { + char *s; + char *strtok_next; + cReadLine ReadLine; + time_t modtime; + while ((s = ReadLine.Read(f)) != NULL) { + char *scrid = strtok_r(s, ";,", &strtok_next); + char *mtime = strtok_r(NULL, ";", &strtok_next); + char *icrids = strtok_r(NULL, ";", &strtok_next); + char *path = strtok_r(NULL, ";", &strtok_next); + char *title = strtok_r(NULL, ";", &strtok_next); + char *channelname = strtok_r(NULL, "`", &strtok_next); + modtime = atoi(mtime); + if ((path != NULL) && (!strcmp(path, "(NULL)"))) path = NULL; + NewLinkItem(scrid, modtime, icrids, path, title, channelname); + } + fclose (f); + isyslog("vdrtva: loaded %d series links", MaxNumber()); + } + else esyslog("vdrtva: series links file not found"); + dirty = false; +} + +void cLinks::Save() +{ + if (!dirty) return; + cString curlinks = AddDirectory(configDir, "links.data"); + cString newlinks = AddDirectory(configDir, "links.new"); + cString oldlinks = AddDirectory(configDir, "links.old"); + FILE *f = fopen(newlinks, "w"); + if (f) { + for (cLinkItem *Item = First(); Item; Item = Next(Item)) { + fprintf(f, "%s;%ld;%s", Item->sCRID(), Item->ModTime(), Item->iCRIDs()); + if (Item->Path()) { + fprintf(f, ";%s", Item->Path()); + } + else fprintf(f, ";(NULL)"); + if (Item->Title()) { + fprintf(f, ";%s", Item->Title()); + } + else fprintf(f, ";(NULL)"); + if (Item->channelName()) { + fprintf(f, ";%s\n", Item->channelName()); + } + else fprintf(f, "\n"); + } + fclose(f); + unlink (oldlinks); // Allow to fail if the save file does not exist + rename (curlinks, oldlinks); + rename (newlinks, curlinks); + dirty = false; + isyslog("vdrtva: saved series links file"); + } +} + +// add a new event to the Links table, either as an addition to an existing series or as a new series. +// return false = nothing done, true = new event for old series, or new series. + +bool cLinks::AddSeriesLink(const char *scrid, time_t modtime, const char *icrid, const char *path, const char *title, const char *channelName) +{ + if (maxNumber >= 1) { + cLinkItem * Item = getLinkItem(scrid); + if (Item != NULL) { + if (strstr(Item->iCRIDs(), icrid) == NULL) { + cString icrids = cString::sprintf("%s:%s", Item->iCRIDs(), icrid); + modtime = max(Item->ModTime(), modtime); + Item->SetModtime(modtime); + Item->SetIcrids(icrids); + isyslog("vdrtva: Adding new event %s to series %s", icrid, scrid); + return true; + } + return false; + } + } + NewLinkItem(scrid, modtime, icrid, path, title, channelName); + isyslog("vdrtva: Creating new series %s for event %s (%s)", scrid, icrid, title); + return true; +} + + +bool cLinks::DeleteItem(const char *sCRID) +{ + if (maxNumber == 0) return false; + cLinkItem *Item = First(); + while (Item) { + cLinkItem *next = Next(Item); + if (!strcmp(Item->sCRID(), sCRID)) { + DeleteTimersForSCRID(sCRID); + Del(Item); + maxNumber--; + dirty = true; + return true; + } + Item = next; + } + return false; +} + +void cLinks::DeleteTimersForSCRID(const char *sCRID) +{ + LOCK_TIMERS_WRITE; + if ((Timers->Count() == 0) || (!captureComplete)) return; + cTimer *ti = Timers->First(); + while (ti) { + cTimer *next = Timers->Next(ti); + const cEvent *event = ti->Event(); + if (event && ti->HasFlags(tfActive) && (ti->WeekDays() == 0)) { + LOCK_CHANNELS_READ; + const cChannel *channel = Channels->GetByChannelID(event->ChannelID()); + cChanDA *chanda = ChanDAs.GetByChannelID(channel->Number()); + cEventCRID *eventcrid = EventCRIDs.GetByID(channel->Number(), event->EventID()); + if (eventcrid && chanda) { + cString scrid = cString::sprintf("%s%s", chanda->DA(),eventcrid->sCRID()); + if (!strcmp(scrid, sCRID)) { + if (!ti->Recording()) { + isyslog ("vdrtva: deleting timer '%s' from deleted series %s", ti->File(), sCRID); + Timers->Del(ti); + Timers->SetModified(); + } + else esyslog("vdrtva: cannot delete timer '%s': timer is recording", ti->File()); + } + } + } + ti = next; + } +} + +void cLinks::Expire(void) +{ + if (maxNumber == 0) return; + cLinkItem *Item = First(); + while (Item) { + cLinkItem *next = Next(Item); + if ((Item->ModTime() + seriesLifetime) < time(NULL)) { + isyslog ("vdrtva: Expiring series %s", Item->sCRID()); + Del(Item); + maxNumber--; + dirty = true; + } + Item = next; + } +} + +cLinkItem * cLinks::getLinkItem(const char *sCRID) +{ + for (cLinkItem *Item = First(); Item; Item = Next(Item)) { + if (strcasecmp(Item->sCRID(), sCRID) == 0) return Item; + } + return NULL; +} + +bool cLinks::isEventNeeded(const char *iCRID) +{ + for (cLinkItem *Item = First(); Item; Item = Next(Item)) { + if (strstr(Item->iCRIDs(), iCRID) != NULL) return false; + } + return true; +} + +void cLinks::SetUpdated(void) +{ + dirty = true; +} + + + +/* + cMenuLinkItem - Series Link OSD menu item +*/ + +cMenuLinkItem::cMenuLinkItem(cLinkItem *LinkItem) +{ + linkitem = LinkItem; + Set(); +} + + +void cMenuLinkItem::Set(void) +{ + cString buffer; + char tim[32]; + struct tm tm_r; + time_t t = linkitem->ModTime(); + tm *tm = localtime_r(&t, &tm_r); + strftime(tim, sizeof(tim), "%d.%m", tm); + if (linkitem->Title()) { + buffer = cString::sprintf("%s\t%s", tim, linkitem->Title()); + } + else { + buffer = cString::sprintf("%s\t(No Title)", tim); + } + SetText(buffer); +} + +int cMenuLinkItem::Compare(const cListObject &ListObject) const +{ + cMenuLinkItem *p = (cMenuLinkItem *)&ListObject; + return linkitem->ModTime() - p->linkitem->ModTime(); +} + +// How many active timers are there for this series? + +int cMenuLinkItem::TimerCount(void) { + int count = 0; + LOCK_TIMERS_READ; + if ((Timers->Count() == 0) || (!captureComplete)) return 99; + for (const cTimer *ti = Timers->First(); ti; ti = Timers->Next(ti)) { + const cEvent *event = ti->Event(); + if (event && ti->HasFlags(tfActive) && (ti->WeekDays() == 0)) { + LOCK_CHANNELS_READ; + const cChannel *channel = Channels->GetByChannelID(event->ChannelID()); + cChanDA *chanda = ChanDAs.GetByChannelID(channel->Number()); + cEventCRID *eventcrid = EventCRIDs.GetByID(channel->Number(), event->EventID()); + if (eventcrid && chanda) { + cString scrid = cString::sprintf("%s%s", chanda->DA(),eventcrid->sCRID()); + if (!strcmp(scrid, sCRID())) count++; + } + } + } + return count; +} + + +/* + cMenuLinks - Series Link OSD menu +*/ + +cMenuLinks::cMenuLinks(void):cOsdMenu(tr("Series Links"), 6) +{ + Clear(); + for (cLinkItem *LinkItem = Links.First(); LinkItem; LinkItem = Links.Next(LinkItem)) { + cMenuLinkItem *item = new cMenuLinkItem(LinkItem); + Add(item); + } + Sort(); + SetHelp(tr("Delete"), tr("Info")); + Display(); +} + +void cMenuLinks::Propagate(void) +{ + for (cMenuLinkItem *ci = (cMenuLinkItem *)First(); ci; ci = (cMenuLinkItem *)ci->Next()) + ci->Set(); + Display(); +} + +eOSState cMenuLinks::ProcessKey(eKeys Key) +{ + eOSState state = cOsdMenu::ProcessKey(Key); + + if (state == osUnknown) { + switch (Key) { + case kRed: return Delete(); + case kGreen: return Info(); + case kYellow: + case kBlue: + case kOk: + default: state = osContinue; + } + } + return state; +} + +eOSState cMenuLinks::Delete(void) +{ + if (HasSubMenu() || Count() == 0) return osContinue; + if (!captureComplete) { + Skins.Message(mtError, tr("Data capture still in progress")); + return osContinue; + } + int Index = Current(); + cMenuLinkItem *item = (cMenuLinkItem *)Get(Index); + int timercount = item->TimerCount(); + cString prompt; + if (timercount > 1) { + prompt = cString::sprintf(tr("Delete series link & %d timers?"), timercount); + } + else if (timercount == 1) { + prompt = cString::sprintf(tr("Delete series link & 1 timer?")); + } + else { + prompt = cString::sprintf(tr("Delete series link?")); + } + if (Interface->Confirm(prompt)) { + char *linkCRID = item->sCRID(); + cOsdMenu::Del(Index); + Propagate(); + isyslog("vdrtva: series link %s deleted by OSD", linkCRID); + Links.DeleteItem(linkCRID); + } + return osContinue; +} + +eOSState cMenuLinks::Info(void) +{ + if (HasSubMenu() || Count() == 0) return osContinue; + if (!captureComplete) { + Skins.Message(mtError, tr("Data capture still in progress")); + return osContinue; + } + int Index = Current(); + cMenuLinkItem *menuitem = (cMenuLinkItem *)Get(Index); + cLinkItem *linkitem = menuitem->LinkItem(); + char *icrids = linkitem->iCRIDs(); + int eventcount = 1; + while (icrids = strchr(icrids, ':')) { + eventcount++; + icrids++; + } + cString message = cString::sprintf("Series CRID: %s\nTotal Events: %d\nActive Timers: %d", + menuitem->sCRID(), eventcount, menuitem->TimerCount()); + if (linkitem->Title()) { + return AddSubMenu(new cMenuText(linkitem->Title(), message, fontOsd)); + } + else { + return AddSubMenu(new cMenuText(tr("(No Title)"), message, fontOsd)); + } +} + + +VDRPLUGINCREATOR(cPluginvdrTva); // Don't touch this! |
