mirror of
https://projects.vdr-developer.org/git/vdr-plugin-streamdev.git
synced 2023-10-10 19:16:51 +02:00
afe255aa0b
with the "red" key. The former main menu action of suspending live TV moved to the "blue" key. Squashed commit of the following: commit7175d7de91
Author: Frank Schmirler <vdr@schmirler.de> Date: Sun Nov 27 11:51:26 2011 +0100 Updated README commit94aef85adc
Author: Frank Schmirler <vdr@schmirler.de> Date: Sun Nov 27 11:32:16 2011 +0100 Moved "closing connection" log message to overload of cTBSocket::Close() in cServerConnection. commit9b91301d94
Author: Frank Schmirler <vdr@schmirler.de> Date: Fri Nov 25 00:24:37 2011 +0100 Don't keep a pointer to the connection in components MulticastGroup structure as the connection may now be deleted from outside via menu. commit7347e24123
Author: Frank Schmirler <vdr@schmirler.de> Date: Thu Nov 24 23:45:59 2011 +0100 Fixed missing Display() call after disconnecting a client. commitc652e8fa81
Author: Frank Schmirler <vdr@schmirler.de> Date: Tue Nov 22 01:15:09 2011 +0100 Added server menu with list of clients. Connections can be terminated with the "red" key. The former main menu action of suspending live TV moved to the "blue" key.
1825 lines
46 KiB
C
1825 lines
46 KiB
C
/*
|
|
* $Id: connectionVTP.c,v 1.31 2010/08/18 10:26:54 schmirl Exp $
|
|
*/
|
|
|
|
#include "server/connectionVTP.h"
|
|
#include "server/livestreamer.h"
|
|
#include "server/suspend.h"
|
|
#include "setup.h"
|
|
|
|
#include <vdr/tools.h>
|
|
#include <vdr/videodir.h>
|
|
#include <vdr/menu.h>
|
|
#include <tools/select.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
|
|
/* VTP Response codes:
|
|
220: Service ready
|
|
221: Service closing connection
|
|
451: Requested action aborted: try again
|
|
500: Syntax error or Command unrecognized
|
|
501: Wrong parameters or missing parameters
|
|
550: Requested action not taken
|
|
551: Data connection not accepted
|
|
560: Channel not available currently
|
|
561: Capability not known
|
|
562: Pid not available currently
|
|
563: Recording not available (currently?)
|
|
*/
|
|
|
|
enum eDumpModeStreamdev { dmsdAll, dmsdPresent, dmsdFollowing, dmsdAtTime, dmsdFromToTime };
|
|
|
|
// --- cLSTEHandler -----------------------------------------------------------
|
|
|
|
class cLSTEHandler
|
|
{
|
|
private:
|
|
#if defined(USE_PARENTALRATING) || defined(PARENTALRATINGCONTENTVERSNUM)
|
|
enum eStates { Channel, Event, Title, Subtitle, Description, Vps, Content,
|
|
EndEvent, EndChannel, EndEPG };
|
|
#elif APIVERSNUM >= 10711
|
|
enum eStates { Channel, Event, Title, Subtitle, Description, Vps, Content, Rating,
|
|
EndEvent, EndChannel, EndEPG };
|
|
#else
|
|
enum eStates { Channel, Event, Title, Subtitle, Description, Vps,
|
|
EndEvent, EndChannel, EndEPG };
|
|
#endif /* PARENTALRATING */
|
|
cConnectionVTP *m_Client;
|
|
cSchedulesLock *m_SchedulesLock;
|
|
const cSchedules *m_Schedules;
|
|
const cSchedule *m_Schedule;
|
|
const cEvent *m_Event;
|
|
int m_Errno;
|
|
cString m_Error;
|
|
eStates m_State;
|
|
bool m_Traverse;
|
|
time_t m_ToTime;
|
|
public:
|
|
cLSTEHandler(cConnectionVTP *Client, const char *Option);
|
|
~cLSTEHandler();
|
|
bool Next(bool &Last);
|
|
};
|
|
|
|
cLSTEHandler::cLSTEHandler(cConnectionVTP *Client, const char *Option):
|
|
m_Client(Client),
|
|
m_SchedulesLock(new cSchedulesLock(false, 500)),
|
|
m_Schedules(cSchedules::Schedules(*m_SchedulesLock)),
|
|
m_Schedule(NULL),
|
|
m_Event(NULL),
|
|
m_Errno(0),
|
|
m_State(Channel),
|
|
m_Traverse(false),
|
|
m_ToTime(0)
|
|
{
|
|
eDumpModeStreamdev dumpmode = dmsdAll;
|
|
time_t attime = 0;
|
|
time_t fromtime = 0;
|
|
|
|
if (m_Schedules != NULL && *Option) {
|
|
char buf[strlen(Option) + 1];
|
|
strcpy(buf, Option);
|
|
const char *delim = " \t";
|
|
char *strtok_next;
|
|
char *p = strtok_r(buf, delim, &strtok_next);
|
|
while (p && dumpmode == dmsdAll) {
|
|
if (strcasecmp(p, "NOW") == 0)
|
|
dumpmode = dmsdPresent;
|
|
else if (strcasecmp(p, "NEXT") == 0)
|
|
dumpmode = dmsdFollowing;
|
|
else if (strcasecmp(p, "AT") == 0) {
|
|
dumpmode = dmsdAtTime;
|
|
if ((p = strtok_r(NULL, delim, &strtok_next)) != NULL) {
|
|
if (isnumber(p))
|
|
attime = strtol(p, NULL, 10);
|
|
else {
|
|
m_Errno = 501;
|
|
m_Error = "Invalid time";
|
|
break;
|
|
}
|
|
} else {
|
|
m_Errno = 501;
|
|
m_Error = "Missing time";
|
|
break;
|
|
}
|
|
}
|
|
else if (strcasecmp(p, "FROM") == 0) {
|
|
dumpmode = dmsdFromToTime;
|
|
if ((p = strtok_r(NULL, delim, &strtok_next)) != NULL) {
|
|
if (isnumber(p))
|
|
fromtime = strtol(p, NULL, 10);
|
|
else {
|
|
m_Errno = 501;
|
|
m_Error = "Invalid time";
|
|
break;
|
|
}
|
|
if ((p = strtok_r(NULL, delim, &strtok_next)) != NULL) {
|
|
if (strcasecmp(p, "TO") == 0) {
|
|
if ((p = strtok_r(NULL, delim, &strtok_next)) != NULL) {
|
|
if (isnumber(p))
|
|
m_ToTime = strtol(p, NULL, 10);
|
|
else {
|
|
m_Errno = 501;
|
|
m_Error = "Invalid time";
|
|
break;
|
|
}
|
|
} else {
|
|
m_Errno = 501;
|
|
m_Error = "Missing time";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
m_Errno = 501;
|
|
m_Error = "Missing time";
|
|
break;
|
|
}
|
|
} else if (!m_Schedule) {
|
|
cChannel* Channel = NULL;
|
|
if (isnumber(p))
|
|
Channel = Channels.GetByNumber(strtol(Option, NULL, 10));
|
|
else
|
|
Channel = Channels.GetByChannelID(tChannelID::FromString(
|
|
Option));
|
|
if (Channel) {
|
|
m_Schedule = m_Schedules->GetSchedule(Channel->GetChannelID());
|
|
if (!m_Schedule) {
|
|
m_Errno = 550;
|
|
m_Error = "No schedule found";
|
|
break;
|
|
}
|
|
} else {
|
|
m_Errno = 550;
|
|
m_Error = cString::sprintf("Channel \"%s\" not defined", p);
|
|
break;
|
|
}
|
|
} else {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Unknown option: \"%s\"", p);
|
|
break;
|
|
}
|
|
p = strtok_r(NULL, delim, &strtok_next);
|
|
}
|
|
} else if (m_Schedules == NULL) {
|
|
m_Errno = 451;
|
|
m_Error = "EPG data is being modified, try again";
|
|
}
|
|
|
|
if (*m_Error == NULL) {
|
|
if (m_Schedule != NULL)
|
|
m_Schedules = NULL;
|
|
else if (m_Schedules != NULL)
|
|
m_Schedule = m_Schedules->First();
|
|
|
|
if (m_Schedule != NULL && m_Schedule->Events() != NULL) {
|
|
switch (dumpmode) {
|
|
case dmsdAll: m_Event = m_Schedule->Events()->First();
|
|
m_Traverse = true;
|
|
break;
|
|
case dmsdPresent: m_Event = m_Schedule->GetPresentEvent();
|
|
break;
|
|
case dmsdFollowing: m_Event = m_Schedule->GetFollowingEvent();
|
|
break;
|
|
case dmsdAtTime: m_Event = m_Schedule->GetEventAround(attime);
|
|
break;
|
|
case dmsdFromToTime:
|
|
if (m_Schedule->Events()->Count() <= 1) {
|
|
m_Event = m_Schedule->Events()->First();
|
|
break;
|
|
}
|
|
if (fromtime < m_Schedule->Events()->First()->StartTime()) {
|
|
fromtime = m_Schedule->Events()->First()->StartTime();
|
|
}
|
|
if (m_ToTime > m_Schedule->Events()->Last()->EndTime()) {
|
|
m_ToTime = m_Schedule->Events()->Last()->EndTime();
|
|
}
|
|
m_Event = m_Schedule->GetEventAround(fromtime);
|
|
m_Traverse = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
cLSTEHandler::~cLSTEHandler()
|
|
{
|
|
delete m_SchedulesLock;
|
|
}
|
|
|
|
bool cLSTEHandler::Next(bool &Last)
|
|
{
|
|
if (*m_Error != NULL) {
|
|
Last = true;
|
|
cString str(m_Error);
|
|
m_Error = NULL;
|
|
return m_Client->Respond(m_Errno, "%s", *str);
|
|
}
|
|
|
|
Last = false;
|
|
switch (m_State) {
|
|
case Channel:
|
|
if (m_Schedule != NULL) {
|
|
cChannel *channel = Channels.GetByChannelID(m_Schedule->ChannelID(),
|
|
true);
|
|
if (channel != NULL) {
|
|
m_State = Event;
|
|
return m_Client->Respond(-215, "C %s %s",
|
|
*channel->GetChannelID().ToString(),
|
|
channel->Name());
|
|
} else {
|
|
esyslog("ERROR: vdr streamdev: unable to find channel %s by ID",
|
|
*m_Schedule->ChannelID().ToString());
|
|
m_State = EndChannel;
|
|
return Next(Last);
|
|
}
|
|
} else {
|
|
m_State = EndEPG;
|
|
return Next(Last);
|
|
}
|
|
break;
|
|
|
|
case Event:
|
|
if (m_Event != NULL) {
|
|
m_State = Title;
|
|
#ifdef __FreeBSD__
|
|
return m_Client->Respond(-215, "E %u %d %d %X", m_Event->EventID(),
|
|
#else
|
|
return m_Client->Respond(-215, "E %u %ld %d %X", m_Event->EventID(),
|
|
#endif
|
|
m_Event->StartTime(), m_Event->Duration(),
|
|
m_Event->TableID());
|
|
} else {
|
|
m_State = EndChannel;
|
|
return Next(Last);
|
|
}
|
|
break;
|
|
|
|
case Title:
|
|
m_State = Subtitle;
|
|
if (!isempty(m_Event->Title()))
|
|
return m_Client->Respond(-215, "T %s", m_Event->Title());
|
|
else
|
|
return Next(Last);
|
|
break;
|
|
|
|
case Subtitle:
|
|
m_State = Description;
|
|
if (!isempty(m_Event->ShortText()))
|
|
return m_Client->Respond(-215, "S %s", m_Event->ShortText());
|
|
else
|
|
return Next(Last);
|
|
break;
|
|
|
|
case Description:
|
|
m_State = Vps;
|
|
if (!isempty(m_Event->Description())) {
|
|
char *copy = strdup(m_Event->Description());
|
|
cString cpy(copy, true);
|
|
strreplace(copy, '\n', '|');
|
|
return m_Client->Respond(-215, "D %s", copy);
|
|
} else
|
|
return Next(Last);
|
|
break;
|
|
|
|
case Vps:
|
|
#if defined(USE_PARENTALRATING) || defined(PARENTALRATINGCONTENTVERSNUM) || APIVERSNUM >= 10711
|
|
m_State = Content;
|
|
#else
|
|
m_State = EndEvent;
|
|
#endif /* PARENTALRATING */
|
|
if (m_Event->Vps())
|
|
#ifdef __FreeBSD__
|
|
return m_Client->Respond(-215, "V %d", m_Event->Vps());
|
|
#else
|
|
return m_Client->Respond(-215, "V %ld", m_Event->Vps());
|
|
#endif
|
|
else
|
|
return Next(Last);
|
|
break;
|
|
|
|
#if defined(USE_PARENTALRATING) || defined(PARENTALRATINGCONTENTVERSNUM)
|
|
case Content:
|
|
m_State = EndEvent;
|
|
if (!isempty(m_Event->GetContentsString())) {
|
|
char *copy = strdup(m_Event->GetContentsString());
|
|
cString cpy(copy, true);
|
|
strreplace(copy, '\n', '|');
|
|
return m_Client->Respond(-215, "G %i %i %s", m_Event->Contents() & 0xF0, m_Event->Contents() & 0x0F, copy);
|
|
} else
|
|
return Next(Last);
|
|
break;
|
|
#elif APIVERSNUM >= 10711
|
|
case Content:
|
|
m_State = Rating;
|
|
if (!isempty(m_Event->ContentToString(m_Event->Contents()))) {
|
|
char *copy = strdup(m_Event->ContentToString(m_Event->Contents()));
|
|
cString cpy(copy, true);
|
|
strreplace(copy, '\n', '|');
|
|
return m_Client->Respond(-215, "G %i %i %s", m_Event->Contents() & 0xF0, m_Event->Contents() & 0x0F, copy);
|
|
} else
|
|
return Next(Last);
|
|
break;
|
|
|
|
case Rating:
|
|
m_State = EndEvent;
|
|
if (m_Event->ParentalRating())
|
|
return m_Client->Respond(-215, "R %d", m_Event->ParentalRating());
|
|
else
|
|
return Next(Last);
|
|
break;
|
|
#endif
|
|
|
|
case EndEvent:
|
|
if (m_Traverse) {
|
|
m_Event = m_Schedule->Events()->Next(m_Event);
|
|
if ((m_Event != NULL) && (m_ToTime != 0) && (m_Event->StartTime() > m_ToTime)) {
|
|
m_Event = NULL;
|
|
}
|
|
}
|
|
else
|
|
m_Event = NULL;
|
|
|
|
if (m_Event != NULL)
|
|
m_State = Event;
|
|
else
|
|
m_State = EndChannel;
|
|
|
|
return m_Client->Respond(-215, "e");
|
|
|
|
case EndChannel:
|
|
if (m_Schedules != NULL) {
|
|
m_Schedule = m_Schedules->Next(m_Schedule);
|
|
if (m_Schedule != NULL) {
|
|
if (m_Schedule->Events() != NULL)
|
|
m_Event = m_Schedule->Events()->First();
|
|
m_State = Channel;
|
|
}
|
|
}
|
|
|
|
if (m_Schedules == NULL || m_Schedule == NULL)
|
|
m_State = EndEPG;
|
|
|
|
return m_Client->Respond(-215, "c");
|
|
|
|
case EndEPG:
|
|
Last = true;
|
|
return m_Client->Respond(215, "End of EPG data");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// --- cLSTCHandler -----------------------------------------------------------
|
|
|
|
class cLSTCHandler
|
|
{
|
|
private:
|
|
cConnectionVTP *m_Client;
|
|
const cChannel *m_Channel;
|
|
char *m_Option;
|
|
int m_Errno;
|
|
cString m_Error;
|
|
bool m_Traverse;
|
|
public:
|
|
cLSTCHandler(cConnectionVTP *Client, const char *Option);
|
|
~cLSTCHandler();
|
|
bool Next(bool &Last);
|
|
};
|
|
|
|
cLSTCHandler::cLSTCHandler(cConnectionVTP *Client, const char *Option):
|
|
m_Client(Client),
|
|
m_Channel(NULL),
|
|
m_Option(NULL),
|
|
m_Errno(0),
|
|
m_Traverse(false)
|
|
{
|
|
if (!Channels.Lock(false, 500)) {
|
|
m_Errno = 451;
|
|
m_Error = "Channels are being modified - try again";
|
|
} else if (*Option) {
|
|
if (isnumber(Option)) {
|
|
m_Channel = Channels.GetByNumber(strtol(Option, NULL, 10));
|
|
if (m_Channel == NULL) {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Channel \"%s\" not defined", Option);
|
|
return;
|
|
}
|
|
} else {
|
|
int i = 1;
|
|
m_Traverse = true;
|
|
m_Option = strdup(Option);
|
|
while (i <= Channels.MaxNumber()) {
|
|
m_Channel = Channels.GetByNumber(i, 1);
|
|
if (strcasestr(m_Channel->Name(), Option) != NULL)
|
|
break;
|
|
i = m_Channel->Number() + 1;
|
|
}
|
|
|
|
if (i > Channels.MaxNumber()) {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Channel \"%s\" not defined", Option);
|
|
return;
|
|
}
|
|
}
|
|
} else if (Channels.MaxNumber() >= 1) {
|
|
m_Channel = Channels.GetByNumber(1, 1);
|
|
m_Traverse = true;
|
|
} else {
|
|
m_Errno = 550;
|
|
m_Error = "No channels defined";
|
|
}
|
|
}
|
|
|
|
cLSTCHandler::~cLSTCHandler()
|
|
{
|
|
Channels.Unlock();
|
|
if (m_Option != NULL)
|
|
free(m_Option);
|
|
}
|
|
|
|
bool cLSTCHandler::Next(bool &Last)
|
|
{
|
|
if (*m_Error != NULL) {
|
|
Last = true;
|
|
cString str(m_Error);
|
|
m_Error = NULL;
|
|
return m_Client->Respond(m_Errno, "%s", *str);
|
|
}
|
|
|
|
int number;
|
|
char *buffer;
|
|
|
|
number = m_Channel->Number();
|
|
buffer = strdup(*m_Channel->ToText());
|
|
buffer[strlen(buffer) - 1] = '\0'; // remove \n
|
|
cString str(buffer, true);
|
|
|
|
Last = true;
|
|
if (m_Traverse) {
|
|
int i = m_Channel->Number() + 1;
|
|
while (i <= Channels.MaxNumber()) {
|
|
m_Channel = Channels.GetByNumber(i, 1);
|
|
if (m_Channel != NULL) {
|
|
if (m_Option == NULL || strcasestr(m_Channel->Name(),
|
|
m_Option) != NULL)
|
|
break;
|
|
i = m_Channel->Number() + 1;
|
|
} else {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Channel \"%d\" not found", i);
|
|
}
|
|
}
|
|
|
|
if (i < Channels.MaxNumber() + 1)
|
|
Last = false;
|
|
}
|
|
|
|
return m_Client->Respond(Last ? 250 : -250, "%d %s", number, buffer);
|
|
}
|
|
|
|
// --- cLSTTHandler -----------------------------------------------------------
|
|
|
|
class cLSTTHandler
|
|
{
|
|
private:
|
|
cConnectionVTP *m_Client;
|
|
cTimer *m_Timer;
|
|
int m_Index;
|
|
int m_Errno;
|
|
cString m_Error;
|
|
bool m_Traverse;
|
|
public:
|
|
cLSTTHandler(cConnectionVTP *Client, const char *Option);
|
|
~cLSTTHandler();
|
|
bool Next(bool &Last);
|
|
};
|
|
|
|
cLSTTHandler::cLSTTHandler(cConnectionVTP *Client, const char *Option):
|
|
m_Client(Client),
|
|
m_Timer(NULL),
|
|
m_Index(0),
|
|
m_Errno(0),
|
|
m_Traverse(false)
|
|
{
|
|
if (*Option) {
|
|
if (isnumber(Option)) {
|
|
m_Timer = Timers.Get(strtol(Option, NULL, 10) - 1);
|
|
if (m_Timer == NULL) {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Timer \"%s\" not defined", Option);
|
|
}
|
|
} else {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Error in timer number \"%s\"", Option);
|
|
}
|
|
} else if (Timers.Count()) {
|
|
m_Traverse = true;
|
|
m_Index = 0;
|
|
m_Timer = Timers.Get(m_Index);
|
|
if (m_Timer == NULL) {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Timer \"%d\" not found", m_Index + 1);
|
|
}
|
|
} else {
|
|
m_Errno = 550;
|
|
m_Error = "No timers defined";
|
|
}
|
|
}
|
|
|
|
cLSTTHandler::~cLSTTHandler()
|
|
{
|
|
}
|
|
|
|
bool cLSTTHandler::Next(bool &Last)
|
|
{
|
|
if (*m_Error != NULL) {
|
|
Last = true;
|
|
cString str(m_Error);
|
|
m_Error = NULL;
|
|
return m_Client->Respond(m_Errno, "%s", *str);
|
|
}
|
|
|
|
bool result;
|
|
char *buffer;
|
|
Last = !m_Traverse || m_Index >= Timers.Count() - 1;
|
|
buffer = strdup(*m_Timer->ToText());
|
|
buffer[strlen(buffer) - 1] = '\0'; // strip \n
|
|
result = m_Client->Respond(Last ? 250 : -250, "%d %s", m_Timer->Index() + 1,
|
|
buffer);
|
|
free(buffer);
|
|
|
|
if (m_Traverse && !Last) {
|
|
m_Timer = Timers.Get(++m_Index);
|
|
if (m_Timer == NULL) {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Timer \"%d\" not found", m_Index + 1);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// --- cLSTRHandler -----------------------------------------------------------
|
|
|
|
class cLSTRHandler
|
|
{
|
|
private:
|
|
enum eStates { Recording, Event, Title, Subtitle, Description, Components, Vps,
|
|
EndRecording };
|
|
cConnectionVTP *m_Client;
|
|
cRecording *m_Recording;
|
|
const cEvent *m_Event;
|
|
int m_Index;
|
|
int m_Errno;
|
|
cString m_Error;
|
|
bool m_Traverse;
|
|
bool m_Info;
|
|
eStates m_State;
|
|
int m_CurrentComponent;
|
|
public:
|
|
cLSTRHandler(cConnectionVTP *Client, const char *Option);
|
|
~cLSTRHandler();
|
|
bool Next(bool &Last);
|
|
};
|
|
|
|
cLSTRHandler::cLSTRHandler(cConnectionVTP *Client, const char *Option):
|
|
m_Client(Client),
|
|
m_Recording(NULL),
|
|
m_Event(NULL),
|
|
m_Index(0),
|
|
m_Errno(0),
|
|
m_Traverse(false),
|
|
m_Info(false),
|
|
m_State(Recording),
|
|
m_CurrentComponent(0)
|
|
{
|
|
if (*Option) {
|
|
if (isnumber(Option)) {
|
|
m_Recording = Recordings.Get(strtol(Option, NULL, 10) - 1);
|
|
#if defined(USE_STREAMDEVEXT) || APIVERSNUM >= 10705
|
|
m_Event = m_Recording->Info()->GetEvent();
|
|
#endif
|
|
m_Info = true;
|
|
if (m_Recording == NULL) {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Recording \"%s\" not found", Option);
|
|
}
|
|
}
|
|
else {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Error in Recording number \"%s\"", Option);
|
|
}
|
|
}
|
|
else if (Recordings.Count()) {
|
|
m_Traverse = true;
|
|
m_Index = 0;
|
|
m_Recording = Recordings.Get(m_Index);
|
|
if (m_Recording == NULL) {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Recording \"%d\" not found", m_Index + 1);
|
|
}
|
|
}
|
|
else {
|
|
m_Errno = 550;
|
|
m_Error = "No recordings available";
|
|
}
|
|
}
|
|
|
|
cLSTRHandler::~cLSTRHandler()
|
|
{
|
|
}
|
|
|
|
bool cLSTRHandler::Next(bool &Last)
|
|
{
|
|
if (*m_Error != NULL) {
|
|
Last = true;
|
|
cString str(m_Error);
|
|
m_Error = NULL;
|
|
return m_Client->Respond(m_Errno, "%s", *str);
|
|
}
|
|
|
|
if (m_Info) {
|
|
Last = false;
|
|
switch (m_State) {
|
|
case Recording:
|
|
if (m_Recording != NULL) {
|
|
m_State = Event;
|
|
return m_Client->Respond(-215, "C %s%s%s",
|
|
*m_Recording->Info()->ChannelID().ToString(),
|
|
m_Recording->Info()->ChannelName() ? " " : "",
|
|
m_Recording->Info()->ChannelName() ? m_Recording->Info()->ChannelName() : "");
|
|
}
|
|
else {
|
|
m_State = EndRecording;
|
|
return Next(Last);
|
|
}
|
|
break;
|
|
|
|
case Event:
|
|
m_State = Title;
|
|
if (m_Event != NULL) {
|
|
return m_Client->Respond(-215, "E %u %ld %d %X %X", (unsigned int) m_Event->EventID(),
|
|
m_Event->StartTime(), m_Event->Duration(),
|
|
m_Event->TableID(), m_Event->Version());
|
|
}
|
|
return Next(Last);
|
|
|
|
case Title:
|
|
m_State = Subtitle;
|
|
return m_Client->Respond(-215, "T %s", m_Recording->Info()->Title());
|
|
|
|
case Subtitle:
|
|
m_State = Description;
|
|
if (!isempty(m_Recording->Info()->ShortText())) {
|
|
return m_Client->Respond(-215, "S %s", m_Recording->Info()->ShortText());
|
|
}
|
|
return Next(Last);
|
|
|
|
case Description:
|
|
m_State = Components;
|
|
if (!isempty(m_Recording->Info()->Description())) {
|
|
m_State = Components;
|
|
char *copy = strdup(m_Recording->Info()->Description());
|
|
cString cpy(copy, true);
|
|
strreplace(copy, '\n', '|');
|
|
return m_Client->Respond(-215, "D %s", copy);
|
|
}
|
|
return Next(Last);
|
|
|
|
case Components:
|
|
if (m_Recording->Info()->Components()) {
|
|
if (m_CurrentComponent < m_Recording->Info()->Components()->NumComponents()) {
|
|
tComponent *p = m_Recording->Info()->Components()->Component(m_CurrentComponent);
|
|
m_CurrentComponent++;
|
|
if (!Setup.UseDolbyDigital && p->stream == 0x02 && p->type == 0x05)
|
|
return Next(Last);
|
|
|
|
return m_Client->Respond(-215, "X %s", *p->ToString());
|
|
}
|
|
}
|
|
m_State = Vps;
|
|
return Next(Last);
|
|
|
|
case Vps:
|
|
m_State = EndRecording;
|
|
if (m_Event != NULL) {
|
|
if (m_Event->Vps()) {
|
|
return m_Client->Respond(-215, "V %ld", m_Event->Vps());
|
|
}
|
|
}
|
|
return Next(Last);
|
|
|
|
case EndRecording:
|
|
Last = true;
|
|
return m_Client->Respond(215, "End of recording information");
|
|
}
|
|
}
|
|
else {
|
|
bool result;
|
|
Last = !m_Traverse || m_Index >= Recordings.Count() - 1;
|
|
result = m_Client->Respond(Last ? 250 : -250, "%d %s", m_Recording->Index() + 1, m_Recording->Title(' ', true));
|
|
|
|
if (m_Traverse && !Last) {
|
|
m_Recording = Recordings.Get(++m_Index);
|
|
if (m_Recording == NULL) {
|
|
m_Errno = 501;
|
|
m_Error = cString::sprintf("Recording \"%d\" not found", m_Index + 1);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// --- cConnectionVTP ---------------------------------------------------------
|
|
|
|
cConnectionVTP::cConnectionVTP(void):
|
|
cServerConnection("VTP"),
|
|
m_LiveSocket(NULL),
|
|
m_LiveStreamer(NULL),
|
|
m_FilterSocket(NULL),
|
|
m_FilterStreamer(NULL),
|
|
m_RecSocket(NULL),
|
|
m_DataSocket(NULL),
|
|
m_LastCommand(NULL),
|
|
m_StreamType(stTSPIDS),
|
|
m_FiltersSupport(false),
|
|
m_RecPlayer(NULL),
|
|
m_TuneChannel(NULL),
|
|
m_TunePriority(0),
|
|
m_LSTEHandler(NULL),
|
|
m_LSTCHandler(NULL),
|
|
m_LSTTHandler(NULL),
|
|
m_LSTRHandler(NULL)
|
|
{
|
|
}
|
|
|
|
cConnectionVTP::~cConnectionVTP()
|
|
{
|
|
if (m_LastCommand != NULL)
|
|
free(m_LastCommand);
|
|
delete m_LiveStreamer;
|
|
delete m_LiveSocket;
|
|
delete m_RecSocket;
|
|
delete m_FilterStreamer;
|
|
delete m_FilterSocket;
|
|
delete m_DataSocket;
|
|
delete m_LSTTHandler;
|
|
delete m_LSTCHandler;
|
|
delete m_LSTEHandler;
|
|
delete m_LSTRHandler;
|
|
delete m_RecPlayer;
|
|
}
|
|
|
|
bool cConnectionVTP::Abort(void) const
|
|
{
|
|
return !IsOpen() || (m_LiveStreamer && m_LiveStreamer->Abort()) ||
|
|
(m_FilterStreamer && m_FilterStreamer->Abort());
|
|
}
|
|
|
|
void cConnectionVTP::Welcome(void)
|
|
{
|
|
Respond(220, "Welcome to Video Disk Recorder (VTP)");
|
|
}
|
|
|
|
void cConnectionVTP::Reject(void)
|
|
{
|
|
Respond(221, "Too many clients or client not allowed to connect");
|
|
cServerConnection::Reject();
|
|
}
|
|
|
|
void cConnectionVTP::Detach(void)
|
|
{
|
|
if (m_LiveStreamer) m_LiveStreamer->Detach();
|
|
if (m_FilterStreamer) m_FilterStreamer->Detach();
|
|
}
|
|
|
|
void cConnectionVTP::Attach(void)
|
|
{
|
|
if (m_LiveStreamer) m_LiveStreamer->Attach();
|
|
if (m_FilterStreamer) m_FilterStreamer->Attach();
|
|
}
|
|
|
|
bool cConnectionVTP::Command(char *Cmd)
|
|
{
|
|
char *param = NULL;
|
|
|
|
if (Cmd != NULL) {
|
|
if (m_LastCommand != NULL) {
|
|
esyslog("ERROR: streamdev: protocol violation (VTP) from %s:%d",
|
|
RemoteIp().c_str(), RemotePort());
|
|
return false;
|
|
}
|
|
|
|
if ((param = strchr(Cmd, ' ')) != NULL)
|
|
*(param++) = '\0';
|
|
else
|
|
param = Cmd + strlen(Cmd);
|
|
m_LastCommand = strdup(Cmd);
|
|
} else {
|
|
Cmd = m_LastCommand;
|
|
param = NULL;
|
|
}
|
|
|
|
if (strcasecmp(Cmd, "LSTE") == 0) return CmdLSTE(param);
|
|
else if (strcasecmp(Cmd, "LSTR") == 0) return CmdLSTR(param);
|
|
else if (strcasecmp(Cmd, "LSTT") == 0) return CmdLSTT(param);
|
|
else if (strcasecmp(Cmd, "LSTC") == 0) return CmdLSTC(param);
|
|
|
|
if (param == NULL) {
|
|
esyslog("ERROR: streamdev: this seriously shouldn't happen at %s:%d",
|
|
__FILE__, __LINE__);
|
|
return false;
|
|
}
|
|
|
|
if (strcasecmp(Cmd, "CAPS") == 0) return CmdCAPS(param);
|
|
else if (strcasecmp(Cmd, "PROV") == 0) return CmdPROV(param);
|
|
else if (strcasecmp(Cmd, "PORT") == 0) return CmdPORT(param);
|
|
else if (strcasecmp(Cmd, "READ") == 0) return CmdREAD(param);
|
|
else if (strcasecmp(Cmd, "TUNE") == 0) return CmdTUNE(param);
|
|
else if (strcasecmp(Cmd, "PLAY") == 0) return CmdPLAY(param);
|
|
else if (strcasecmp(Cmd, "PRIO") == 0) return CmdPRIO(param);
|
|
else if (strcasecmp(Cmd, "SGNL") == 0) return CmdSGNL(param);
|
|
else if (strcasecmp(Cmd, "ADDP") == 0) return CmdADDP(param);
|
|
else if (strcasecmp(Cmd, "DELP") == 0) return CmdDELP(param);
|
|
else if (strcasecmp(Cmd, "ADDF") == 0) return CmdADDF(param);
|
|
else if (strcasecmp(Cmd, "DELF") == 0) return CmdDELF(param);
|
|
else if (strcasecmp(Cmd, "ABRT") == 0) return CmdABRT(param);
|
|
else if (strcasecmp(Cmd, "QUIT") == 0) return CmdQUIT();
|
|
else if (strcasecmp(Cmd, "SUSP") == 0) return CmdSUSP();
|
|
// Commands adopted from SVDRP
|
|
else if (strcasecmp(Cmd, "STAT") == 0) return CmdSTAT(param);
|
|
else if (strcasecmp(Cmd, "MODT") == 0) return CmdMODT(param);
|
|
else if (strcasecmp(Cmd, "NEWT") == 0) return CmdNEWT(param);
|
|
else if (strcasecmp(Cmd, "DELT") == 0) return CmdDELT(param);
|
|
else if (strcasecmp(Cmd, "NEXT") == 0) return CmdNEXT(param);
|
|
else if (strcasecmp(Cmd, "NEWC") == 0) return CmdNEWC(param);
|
|
else if (strcasecmp(Cmd, "MODC") == 0) return CmdMODC(param);
|
|
else if (strcasecmp(Cmd, "MOVC") == 0) return CmdMOVC(param);
|
|
else if (strcasecmp(Cmd, "DELC") == 0) return CmdDELC(param);
|
|
else if (strcasecmp(Cmd, "DELR") == 0) return CmdDELR(param);
|
|
else if (strcasecmp(Cmd, "RENR") == 0) return CmdRENR(param);
|
|
else
|
|
return Respond(500, "Unknown Command \"%s\"", Cmd);
|
|
}
|
|
|
|
bool cConnectionVTP::CmdCAPS(char *Opts)
|
|
{
|
|
if (strcasecmp(Opts, "TS") == 0) {
|
|
m_StreamType = stTS;
|
|
return Respond(220, "Capability \"%s\" accepted", Opts);
|
|
}
|
|
|
|
if (strcasecmp(Opts, "TSPIDS") == 0) {
|
|
m_StreamType = stTSPIDS;
|
|
return Respond(220, "Capability \"%s\" accepted", Opts);
|
|
}
|
|
|
|
if (strcasecmp(Opts, "PS") == 0) {
|
|
m_StreamType = stPS;
|
|
return Respond(220, "Capability \"%s\" accepted", Opts);
|
|
}
|
|
|
|
if (strcasecmp(Opts, "PES") == 0) {
|
|
m_StreamType = stPES;
|
|
return Respond(220, "Capability \"%s\" accepted", Opts);
|
|
}
|
|
|
|
if (strcasecmp(Opts, "EXT") == 0) {
|
|
m_StreamType = stEXT;
|
|
return Respond(220, "Capability \"%s\" accepted", Opts);
|
|
}
|
|
|
|
//
|
|
// Deliver section filters data in separate, channel-independent data stream
|
|
//
|
|
if (strcasecmp(Opts, "FILTERS") == 0) {
|
|
m_FiltersSupport = true;
|
|
return Respond(220, "Capability \"%s\" accepted", Opts);
|
|
}
|
|
|
|
// Command PRIO is known
|
|
if (strcasecmp(Opts, "PRIO") == 0) {
|
|
return Respond(220, "Capability \"%s\" accepted", Opts);
|
|
}
|
|
|
|
return Respond(561, "Capability \"%s\" not known", Opts);
|
|
}
|
|
|
|
bool cConnectionVTP::CmdPROV(char *Opts)
|
|
{
|
|
const cChannel *chan;
|
|
int prio;
|
|
char *ep;
|
|
|
|
prio = strtol(Opts, &ep, 10);
|
|
if (ep == Opts || !isspace(*ep))
|
|
return Respond(501, "Use: PROV Priority Channel");
|
|
|
|
Opts = skipspace(ep);
|
|
if ((chan = ChannelFromString(Opts)) == NULL)
|
|
return Respond(550, "Undefined channel \"%s\"", Opts);
|
|
|
|
if (ProvidesChannel(chan, prio)) {
|
|
m_TuneChannel = chan;
|
|
m_TunePriority = prio;
|
|
return Respond(220, "Channel available");
|
|
}
|
|
else {
|
|
m_TuneChannel = NULL;
|
|
return Respond(560, "Channel not available");
|
|
}
|
|
}
|
|
|
|
bool cConnectionVTP::CmdPORT(char *Opts)
|
|
{
|
|
uint id, dataport = 0;
|
|
char dataip[20];
|
|
char *ep, *ipoffs;
|
|
int n;
|
|
|
|
id = strtoul(Opts, &ep, 10);
|
|
if (ep == Opts || !isspace(*ep))
|
|
return Respond(500, "Use: PORT Id Destination");
|
|
|
|
if (id >= si_Count)
|
|
return Respond(501, "Wrong connection id %d", id);
|
|
|
|
Opts = skipspace(ep);
|
|
n = 0;
|
|
ipoffs = dataip;
|
|
while ((ep = strchr(Opts, ',')) != NULL) {
|
|
if (n < 4) {
|
|
memcpy(ipoffs, Opts, ep - Opts);
|
|
ipoffs += ep - Opts;
|
|
if (n < 3) *(ipoffs++) = '.';
|
|
} else if (n == 4) {
|
|
*ep = 0;
|
|
dataport = strtoul(Opts, NULL, 10) << 8;
|
|
} else
|
|
break;
|
|
Opts = ep + 1;
|
|
++n;
|
|
}
|
|
*ipoffs = '\0';
|
|
|
|
if (n != 5)
|
|
return Respond(501, "Argument count invalid (must be 6 values)");
|
|
|
|
dataport |= strtoul(Opts, NULL, 10);
|
|
|
|
isyslog("Streamdev: Setting data connection to %s:%d", dataip, dataport);
|
|
|
|
switch (id) {
|
|
case siLiveFilter:
|
|
m_FiltersSupport = true;
|
|
if(m_FilterStreamer)
|
|
m_FilterStreamer->Stop();
|
|
delete m_FilterSocket;
|
|
|
|
m_FilterSocket = new cTBSocket(SOCK_STREAM);
|
|
if (!m_FilterSocket->Connect(dataip, dataport)) {
|
|
esyslog("ERROR: Streamdev: Couldn't open data connection to %s:%d: %s",
|
|
dataip, dataport, strerror(errno));
|
|
DELETENULL(m_FilterSocket);
|
|
return Respond(551, "Couldn't open data connection");
|
|
}
|
|
|
|
if(!m_FilterStreamer)
|
|
m_FilterStreamer = new cStreamdevFilterStreamer;
|
|
m_FilterStreamer->Start(m_FilterSocket);
|
|
m_FilterStreamer->Activate(true);
|
|
|
|
return Respond(220, "Port command ok, data connection opened");
|
|
break;
|
|
|
|
case siLive:
|
|
if(m_LiveSocket && m_LiveStreamer)
|
|
m_LiveStreamer->Stop();
|
|
delete m_LiveSocket;
|
|
|
|
m_LiveSocket = new cTBSocket(SOCK_STREAM);
|
|
if (!m_LiveSocket->Connect(dataip, dataport)) {
|
|
esyslog("ERROR: Streamdev: Couldn't open data connection to %s:%d: %s",
|
|
dataip, dataport, strerror(errno));
|
|
DELETENULL(m_LiveSocket);
|
|
return Respond(551, "Couldn't open data connection");
|
|
}
|
|
|
|
if (!m_LiveSocket->SetDSCP())
|
|
LOG_ERROR_STR("unable to set DSCP sockopt");
|
|
if (m_LiveStreamer)
|
|
m_LiveStreamer->Start(m_LiveSocket);
|
|
|
|
return Respond(220, "Port command ok, data connection opened");
|
|
break;
|
|
|
|
case siReplay:
|
|
delete m_RecSocket;
|
|
|
|
m_RecSocket = new cTBSocket(SOCK_STREAM);
|
|
if (!m_RecSocket->Connect(dataip, dataport)) {
|
|
esyslog("ERROR: Streamdev: Couldn't open data connection to %s:%d: %s",
|
|
dataip, dataport, strerror(errno));
|
|
DELETENULL(m_RecSocket);
|
|
return Respond(551, "Couldn't open data connection");
|
|
}
|
|
|
|
if (!m_RecSocket->SetDSCP())
|
|
LOG_ERROR_STR("unable to set DSCP sockopt");
|
|
|
|
return Respond(220, "Port command ok, data connection opened");
|
|
break;
|
|
|
|
case siDataRespond:
|
|
delete m_DataSocket;
|
|
|
|
m_DataSocket = new cTBSocket(SOCK_STREAM);
|
|
if (!m_DataSocket->Connect(dataip, dataport)) {
|
|
esyslog("ERROR: Streamdev: Couldn't open data connection to %s:%d: %s",
|
|
dataip, dataport, strerror(errno));
|
|
DELETENULL(m_DataSocket);
|
|
return Respond(551, "Couldn't open data connection");
|
|
}
|
|
return Respond(220, "Port command ok, data connection opened");
|
|
break;
|
|
|
|
default:
|
|
return Respond(501, "No handler for id %u", id);
|
|
}
|
|
}
|
|
|
|
bool cConnectionVTP::CmdREAD(char *Opts)
|
|
{
|
|
if (*Opts) {
|
|
char *tail;
|
|
uint64_t position = strtoll(Opts, &tail, 10);
|
|
if (tail && tail != Opts) {
|
|
tail = skipspace(tail);
|
|
if (tail && tail != Opts) {
|
|
int size = strtol(tail, NULL, 10);
|
|
uint8_t* data = (uint8_t*)malloc(size+4);
|
|
unsigned long count_readed = m_RecPlayer->getBlock(data, position, size);
|
|
unsigned long count_written = m_RecSocket->SysWrite(data, count_readed);
|
|
|
|
free(data);
|
|
return Respond(220, "%lu Bytes submitted", count_written);
|
|
}
|
|
else {
|
|
return Respond(501, "Missing position");
|
|
}
|
|
}
|
|
else {
|
|
return Respond(501, "Missing size");
|
|
}
|
|
}
|
|
else {
|
|
return Respond(501, "Missing position");
|
|
}
|
|
}
|
|
|
|
bool cConnectionVTP::CmdTUNE(char *Opts)
|
|
{
|
|
const cChannel *chan;
|
|
cDevice *dev;
|
|
int prio = m_TunePriority;
|
|
|
|
if ((chan = ChannelFromString(Opts)) == NULL)
|
|
return Respond(550, "Undefined channel \"%s\"", Opts);
|
|
|
|
if (chan != m_TuneChannel) {
|
|
isyslog("streamdev-server TUNE %s: Priority unknown - using 0", Opts);
|
|
prio = 0;
|
|
if (!ProvidesChannel(chan, prio))
|
|
return Respond(560, "Channel not available (ProvidesChannel)");
|
|
}
|
|
if ((dev = GetDevice(chan, prio)) == NULL)
|
|
return Respond(560, "Channel not available (GetDevice)");
|
|
|
|
if (!dev->SwitchChannel(chan, false))
|
|
return Respond(560, "Channel not available (SwitchChannel)");
|
|
|
|
delete m_LiveStreamer;
|
|
m_LiveStreamer = new cStreamdevLiveStreamer(prio, this);
|
|
m_LiveStreamer->SetChannel(chan, m_StreamType);
|
|
m_LiveStreamer->SetDevice(dev);
|
|
if(m_LiveSocket)
|
|
m_LiveStreamer->Start(m_LiveSocket);
|
|
|
|
if(m_FiltersSupport) {
|
|
if(!m_FilterStreamer)
|
|
m_FilterStreamer = new cStreamdevFilterStreamer;
|
|
m_FilterStreamer->SetDevice(dev);
|
|
//m_FilterStreamer->SetChannel(chan);
|
|
}
|
|
|
|
return Respond(220, "Channel tuned");
|
|
}
|
|
|
|
bool cConnectionVTP::CmdPLAY(char *Opts)
|
|
{
|
|
if (*Opts) {
|
|
if (isnumber(Opts)) {
|
|
cRecording *recording = Recordings.Get(strtol(Opts, NULL, 10) - 1);
|
|
if (recording) {
|
|
if (m_RecPlayer) {
|
|
delete m_RecPlayer;
|
|
}
|
|
m_RecPlayer = new RecPlayer(recording);
|
|
return Respond(220, "%llu (Bytes), %u (Frames)", (long long unsigned int) m_RecPlayer->getLengthBytes(), (unsigned int) m_RecPlayer->getLengthFrames());
|
|
}
|
|
else {
|
|
return Respond(550, "Recording \"%s\" not found", Opts);
|
|
}
|
|
}
|
|
else {
|
|
return Respond(500, "Use: PLAY record");
|
|
}
|
|
}
|
|
else {
|
|
return Respond(500, "Use: PLAY record");
|
|
}
|
|
}
|
|
|
|
bool cConnectionVTP::CmdPRIO(char *Opts)
|
|
{
|
|
int prio;
|
|
char *end;
|
|
|
|
prio = strtoul(Opts, &end, 10);
|
|
if (end == Opts || (*end != '\0' && *end != ' '))
|
|
return Respond(500, "Use: PRIO Priority");
|
|
|
|
if (m_LiveStreamer) {
|
|
m_LiveStreamer->SetPriority(prio);
|
|
return Respond(220, "Priority changed to %d", prio);
|
|
}
|
|
return Respond(550, "Priority not applicable");
|
|
}
|
|
|
|
bool cConnectionVTP::CmdSGNL(char *Opts)
|
|
{
|
|
if (m_LiveStreamer) {
|
|
int devnum = -1;
|
|
int signal = -1;
|
|
int quality = -1;
|
|
m_LiveStreamer->GetSignal(&devnum, &signal, &quality);
|
|
return Respond(220, "%d %d:%d", devnum, signal, quality);
|
|
}
|
|
return Respond(550, "Signal not applicable");
|
|
}
|
|
|
|
bool cConnectionVTP::CmdADDP(char *Opts)
|
|
{
|
|
int pid;
|
|
char *end;
|
|
|
|
pid = strtoul(Opts, &end, 10);
|
|
if (end == Opts || (*end != '\0' && *end != ' '))
|
|
return Respond(500, "Use: ADDP Pid");
|
|
|
|
return m_LiveStreamer && m_LiveStreamer->SetPid(pid, true)
|
|
? Respond(220, "Pid %d available", pid)
|
|
: Respond(560, "Pid %d not available", pid);
|
|
}
|
|
|
|
bool cConnectionVTP::CmdDELP(char *Opts)
|
|
{
|
|
int pid;
|
|
char *end;
|
|
|
|
pid = strtoul(Opts, &end, 10);
|
|
if (end == Opts || (*end != '\0' && *end != ' '))
|
|
return Respond(500, "Use: DELP Pid");
|
|
|
|
return m_LiveStreamer && m_LiveStreamer->SetPid(pid, false)
|
|
? Respond(220, "Pid %d stopped", pid)
|
|
: Respond(560, "Pid %d not transferring", pid);
|
|
}
|
|
|
|
bool cConnectionVTP::CmdADDF(char *Opts)
|
|
{
|
|
int pid, tid, mask;
|
|
char *ep;
|
|
|
|
if (m_FilterStreamer == NULL)
|
|
return Respond(560, "Can't set filters without a filter stream");
|
|
|
|
pid = strtol(Opts, &ep, 10);
|
|
if (ep == Opts || (*ep != ' '))
|
|
return Respond(500, "Use: ADDF Pid Tid Mask");
|
|
Opts = skipspace(ep);
|
|
tid = strtol(Opts, &ep, 10);
|
|
if (ep == Opts || (*ep != ' '))
|
|
return Respond(500, "Use: ADDF Pid Tid Mask");
|
|
Opts = skipspace(ep);
|
|
mask = strtol(Opts, &ep, 10);
|
|
if (ep == Opts || (*ep != '\0' && *ep != ' '))
|
|
return Respond(500, "Use: ADDF Pid Tid Mask");
|
|
|
|
return m_FilterStreamer->SetFilter(pid, tid, mask, true)
|
|
? Respond(220, "Filter %d transferring", pid)
|
|
: Respond(560, "Filter %d not available", pid);
|
|
}
|
|
|
|
bool cConnectionVTP::CmdDELF(char *Opts)
|
|
{
|
|
int pid, tid, mask;
|
|
char *ep;
|
|
|
|
if (m_FilterStreamer == NULL)
|
|
return Respond(560, "Can't delete filters without a stream");
|
|
|
|
pid = strtol(Opts, &ep, 10);
|
|
if (ep == Opts || (*ep != ' '))
|
|
return Respond(500, "Use: DELF Pid Tid Mask");
|
|
Opts = skipspace(ep);
|
|
tid = strtol(Opts, &ep, 10);
|
|
if (ep == Opts || (*ep != ' '))
|
|
return Respond(500, "Use: DELF Pid Tid Mask");
|
|
Opts = skipspace(ep);
|
|
mask = strtol(Opts, &ep, 10);
|
|
if (ep == Opts || (*ep != '\0' && *ep != ' '))
|
|
return Respond(500, "Use: DELF Pid Tid Mask");
|
|
|
|
m_FilterStreamer->SetFilter(pid, tid, mask, false);
|
|
return Respond(220, "Filter %d stopped", pid);
|
|
}
|
|
|
|
bool cConnectionVTP::CmdABRT(char *Opts)
|
|
{
|
|
uint id;
|
|
char *ep;
|
|
|
|
id = strtoul(Opts, &ep, 10);
|
|
if (ep == Opts || (*ep != '\0' && *ep != ' '))
|
|
return Respond(500, "Use: ABRT Id");
|
|
|
|
switch (id) {
|
|
case siLive:
|
|
DELETENULL(m_LiveStreamer);
|
|
DELETENULL(m_LiveSocket);
|
|
break;
|
|
case siLiveFilter:
|
|
DELETENULL(m_FilterStreamer);
|
|
DELETENULL(m_FilterSocket);
|
|
break;
|
|
case siReplay:
|
|
DELETENULL(m_RecPlayer);
|
|
DELETENULL(m_RecSocket);
|
|
break;
|
|
case siDataRespond:
|
|
DELETENULL(m_DataSocket);
|
|
break;
|
|
default:
|
|
return Respond(501, "Wrong connection id %d", id);
|
|
break;
|
|
|
|
}
|
|
|
|
return Respond(220, "Data connection closed");
|
|
}
|
|
|
|
bool cConnectionVTP::CmdQUIT(void)
|
|
{
|
|
DeferClose();
|
|
return Respond(221, "Video Disk Recorder closing connection");
|
|
}
|
|
|
|
bool cConnectionVTP::CmdSUSP(void)
|
|
{
|
|
if (StreamdevServerSetup.SuspendMode == smAlways || cSuspendCtl::IsActive())
|
|
return Respond(220, "Server is suspended");
|
|
else if (StreamdevServerSetup.SuspendMode == smOffer
|
|
&& StreamdevServerSetup.AllowSuspend) {
|
|
cControl::Launch(new cSuspendCtl);
|
|
cControl::Attach();
|
|
return Respond(220, "Server is suspended");
|
|
} else
|
|
return Respond(550, "Client may not suspend server");
|
|
}
|
|
|
|
// Functions extended from SVDRP
|
|
|
|
template<class cHandler>
|
|
bool cConnectionVTP::CmdLSTX(cHandler *&Handler, char *Option)
|
|
{
|
|
if (Option != NULL) {
|
|
delete Handler;
|
|
Handler = new cHandler(this, Option);
|
|
}
|
|
|
|
bool last = false;
|
|
bool result = false;
|
|
if (Handler != NULL)
|
|
result = Handler->Next(last);
|
|
else
|
|
esyslog("ERROR: vdr streamdev: Handler in LSTX command is NULL");
|
|
if (!result || last)
|
|
DELETENULL(Handler);
|
|
|
|
return result;
|
|
}
|
|
|
|
bool cConnectionVTP::CmdLSTE(char *Option)
|
|
{
|
|
return CmdLSTX(m_LSTEHandler, Option);
|
|
}
|
|
|
|
bool cConnectionVTP::CmdLSTC(char *Option)
|
|
{
|
|
return CmdLSTX(m_LSTCHandler, Option);
|
|
}
|
|
|
|
bool cConnectionVTP::CmdLSTT(char *Option)
|
|
{
|
|
return CmdLSTX(m_LSTTHandler, Option);
|
|
}
|
|
|
|
bool cConnectionVTP::CmdLSTR(char *Option)
|
|
{
|
|
return CmdLSTX(m_LSTRHandler, Option);
|
|
}
|
|
|
|
// Functions adopted from SVDRP
|
|
#define INIT_WRAPPER() bool _res
|
|
#define Reply(c,m...) _res = Respond(c,m)
|
|
#define EXIT_WRAPPER() return _res
|
|
|
|
bool cConnectionVTP::CmdSTAT(const char *Option)
|
|
{
|
|
INIT_WRAPPER();
|
|
if (*Option) {
|
|
if (strcasecmp(Option, "DISK") == 0) {
|
|
int FreeMB, UsedMB;
|
|
int Percent = VideoDiskSpace(&FreeMB, &UsedMB);
|
|
Reply(250, "%dMB %dMB %d%%", FreeMB + UsedMB, FreeMB, Percent);
|
|
}
|
|
else if (strcasecmp(Option, "NAME") == 0) {
|
|
Reply(250, "vdr - The Video Disk Recorder with Streamdev-Server");
|
|
}
|
|
else if (strcasecmp(Option, "VERSION") == 0) {
|
|
Reply(250, "VDR: %s | Streamdev: %s", VDRVERSION, VERSION);
|
|
}
|
|
else if (strcasecmp(Option, "RECORDS") == 0) {
|
|
bool recordings = Recordings.Load();
|
|
Recordings.Sort();
|
|
if (recordings) {
|
|
cRecording *recording = Recordings.Last();
|
|
Reply(250, "%d", recording->Index() + 1);
|
|
}
|
|
else {
|
|
Reply(250, "0");
|
|
}
|
|
}
|
|
else if (strcasecmp(Option, "CHANNELS") == 0) {
|
|
Reply(250, "%d", Channels.MaxNumber());
|
|
}
|
|
else if (strcasecmp(Option, "TIMERS") == 0) {
|
|
Reply(250, "%d", Timers.Count());
|
|
}
|
|
else if (strcasecmp(Option, "CHARSET") == 0) {
|
|
Reply(250, "%s", cCharSetConv::SystemCharacterTable());
|
|
}
|
|
else if (strcasecmp(Option, "TIME") == 0) {
|
|
time_t timeNow = time(NULL);
|
|
struct tm* timeStruct = localtime(&timeNow);
|
|
int timeOffset = timeStruct->tm_gmtoff;
|
|
|
|
Reply(250, "%lu %i", (unsigned long) timeNow, timeOffset);
|
|
}
|
|
else
|
|
Reply(501, "Invalid Option \"%s\"", Option);
|
|
}
|
|
else
|
|
Reply(501, "No option given");
|
|
EXIT_WRAPPER();
|
|
}
|
|
|
|
bool cConnectionVTP::CmdMODT(const char *Option)
|
|
{
|
|
INIT_WRAPPER();
|
|
if (*Option) {
|
|
char *tail;
|
|
int n = strtol(Option, &tail, 10);
|
|
if (tail && tail != Option) {
|
|
tail = skipspace(tail);
|
|
cTimer *timer = Timers.Get(n - 1);
|
|
if (timer) {
|
|
cTimer t = *timer;
|
|
if (strcasecmp(tail, "ON") == 0)
|
|
t.SetFlags(tfActive);
|
|
else if (strcasecmp(tail, "OFF") == 0)
|
|
t.ClrFlags(tfActive);
|
|
else if (!t.Parse(tail)) {
|
|
Reply(501, "Error in timer settings");
|
|
EXIT_WRAPPER();
|
|
}
|
|
*timer = t;
|
|
Timers.SetModified();
|
|
isyslog("timer %s modified (%s)", *timer->ToDescr(),
|
|
timer->HasFlags(tfActive) ? "active" : "inactive");
|
|
Reply(250, "%d %s", timer->Index() + 1, *timer->ToText());
|
|
} else
|
|
Reply(501, "Timer \"%d\" not defined", n);
|
|
} else
|
|
Reply(501, "Error in timer number");
|
|
} else
|
|
Reply(501, "Missing timer settings");
|
|
EXIT_WRAPPER();
|
|
}
|
|
|
|
bool cConnectionVTP::CmdNEWT(const char *Option)
|
|
{
|
|
INIT_WRAPPER();
|
|
if (*Option) {
|
|
cTimer *timer = new cTimer;
|
|
if (timer->Parse(Option)) {
|
|
cTimer *t = Timers.GetTimer(timer);
|
|
if (!t) {
|
|
Timers.Add(timer);
|
|
Timers.SetModified();
|
|
isyslog("timer %s added", *timer->ToDescr());
|
|
Reply(250, "%d %s", timer->Index() + 1, *timer->ToText());
|
|
EXIT_WRAPPER();
|
|
} else
|
|
Reply(550, "Timer already defined: %d %s", t->Index() + 1,
|
|
*t->ToText());
|
|
} else
|
|
Reply(501, "Error in timer settings");
|
|
delete timer;
|
|
} else
|
|
Reply(501, "Missing timer settings");
|
|
EXIT_WRAPPER();
|
|
}
|
|
|
|
bool cConnectionVTP::CmdDELT(const char *Option)
|
|
{
|
|
INIT_WRAPPER();
|
|
if (*Option) {
|
|
int number = 0;
|
|
bool force = false;
|
|
char buf[strlen(Option) + 1];
|
|
strcpy(buf, Option);
|
|
const char *delim = " \t";
|
|
char *strtok_next;
|
|
char *p = strtok_r(buf, delim, &strtok_next);
|
|
|
|
if (isnumber(p)) {
|
|
number = strtol(p, NULL, 10) - 1;
|
|
}
|
|
else if (strcasecmp(p, "FORCE") == 0) {
|
|
force = true;
|
|
}
|
|
if ((p = strtok_r(NULL, delim, &strtok_next)) != NULL) {
|
|
if (isnumber(p)) {
|
|
number = strtol(p, NULL, 10) - 1;
|
|
}
|
|
else if (strcasecmp(p, "FORCE") == 0) {
|
|
force = true;
|
|
}
|
|
else {
|
|
Reply(501, "Timer not found or wrong syntax");
|
|
}
|
|
}
|
|
|
|
cTimer *timer = Timers.Get(number);
|
|
if (timer) {
|
|
if (timer->Recording()) {
|
|
if (force) {
|
|
timer->Skip();
|
|
cRecordControls::Process(time(NULL));
|
|
}
|
|
else {
|
|
Reply(550, "Timer \"%i\" is recording", number);
|
|
EXIT_WRAPPER();
|
|
}
|
|
}
|
|
isyslog("deleting timer %s", *timer->ToDescr());
|
|
Timers.Del(timer);
|
|
Timers.SetModified();
|
|
Reply(250, "Timer \"%i\" deleted", number);
|
|
} else
|
|
Reply(501, "Timer \"%i\" not defined", number);
|
|
} else
|
|
Reply(501, "Missing timer option");
|
|
EXIT_WRAPPER();
|
|
}
|
|
|
|
bool cConnectionVTP::CmdNEXT(const char *Option)
|
|
{
|
|
INIT_WRAPPER();
|
|
cTimer *t = Timers.GetNextActiveTimer();
|
|
if (t) {
|
|
time_t Start = t->StartTime();
|
|
int Number = t->Index() + 1;
|
|
if (!*Option)
|
|
Reply(250, "%d %s", Number, *TimeToString(Start));
|
|
else if (strcasecmp(Option, "ABS") == 0)
|
|
Reply(250, "%d %ld", Number, Start);
|
|
else if (strcasecmp(Option, "REL") == 0)
|
|
Reply(250, "%d %ld", Number, Start - time(NULL));
|
|
else
|
|
Reply(501, "Unknown option: \"%s\"", Option);
|
|
}
|
|
else
|
|
Reply(550, "No active timers");
|
|
EXIT_WRAPPER();
|
|
}
|
|
|
|
bool cConnectionVTP::CmdNEWC(const char *Option)
|
|
{
|
|
INIT_WRAPPER();
|
|
if (*Option) {
|
|
cChannel ch;
|
|
if (ch.Parse(Option)) {
|
|
if (Channels.HasUniqueChannelID(&ch)) {
|
|
cChannel *channel = new cChannel;
|
|
*channel = ch;
|
|
Channels.Add(channel);
|
|
Channels.ReNumber();
|
|
Channels.SetModified(true);
|
|
isyslog("new channel %d %s", channel->Number(), *channel->ToText());
|
|
Reply(250, "%d %s", channel->Number(), *channel->ToText());
|
|
}
|
|
else {
|
|
Reply(501, "Channel settings are not unique");
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Error in channel settings");
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Missing channel settings");
|
|
}
|
|
EXIT_WRAPPER();
|
|
}
|
|
|
|
bool cConnectionVTP::CmdMODC(const char *Option)
|
|
{
|
|
INIT_WRAPPER();
|
|
if (*Option) {
|
|
char *tail;
|
|
int n = strtol(Option, &tail, 10);
|
|
if (tail && tail != Option) {
|
|
tail = skipspace(tail);
|
|
if (!Channels.BeingEdited()) {
|
|
cChannel *channel = Channels.GetByNumber(n);
|
|
if (channel) {
|
|
cChannel ch;
|
|
if (ch.Parse(tail)) {
|
|
if (Channels.HasUniqueChannelID(&ch, channel)) {
|
|
*channel = ch;
|
|
Channels.ReNumber();
|
|
Channels.SetModified(true);
|
|
isyslog("modifed channel %d %s", channel->Number(), *channel->ToText());
|
|
Reply(250, "%d %s", channel->Number(), *channel->ToText());
|
|
}
|
|
else {
|
|
Reply(501, "Channel settings are not unique");
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Error in channel settings");
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Channel \"%d\" not defined", n);
|
|
}
|
|
}
|
|
else {
|
|
Reply(550, "Channels are being edited - try again later");
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Error in channel number");
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Missing channel settings");
|
|
}
|
|
EXIT_WRAPPER();
|
|
}
|
|
|
|
bool cConnectionVTP::CmdMOVC(const char *Option)
|
|
{
|
|
INIT_WRAPPER();
|
|
if (*Option) {
|
|
if (!Channels.BeingEdited() && !Timers.BeingEdited()) {
|
|
char *tail;
|
|
int From = strtol(Option, &tail, 10);
|
|
if (tail && tail != Option) {
|
|
tail = skipspace(tail);
|
|
if (tail && tail != Option) {
|
|
int To = strtol(tail, NULL, 10);
|
|
int CurrentChannelNr = cDevice::CurrentChannel();
|
|
cChannel *CurrentChannel = Channels.GetByNumber(CurrentChannelNr);
|
|
cChannel *FromChannel = Channels.GetByNumber(From);
|
|
if (FromChannel) {
|
|
cChannel *ToChannel = Channels.GetByNumber(To);
|
|
if (ToChannel) {
|
|
int FromNumber = FromChannel->Number();
|
|
int ToNumber = ToChannel->Number();
|
|
if (FromNumber != ToNumber) {
|
|
Channels.Move(FromChannel, ToChannel);
|
|
Channels.ReNumber();
|
|
Channels.SetModified(true);
|
|
if (CurrentChannel && CurrentChannel->Number() != CurrentChannelNr) {
|
|
if (!cDevice::PrimaryDevice()->Replaying() || cDevice::PrimaryDevice()->Transferring()) {
|
|
Channels.SwitchTo(CurrentChannel->Number());
|
|
}
|
|
else {
|
|
cDevice::SetCurrentChannel(CurrentChannel);
|
|
}
|
|
}
|
|
isyslog("channel %d moved to %d", FromNumber, ToNumber);
|
|
Reply(250,"Channel \"%d\" moved to \"%d\"", From, To);
|
|
}
|
|
else {
|
|
Reply(501, "Can't move channel to same postion");
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Channel \"%d\" not defined", To);
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Channel \"%d\" not defined", From);
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Error in channel number");
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Error in channel number");
|
|
}
|
|
}
|
|
else {
|
|
Reply(550, "Channels or timers are being edited - try again later");
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Missing channel number");
|
|
}
|
|
EXIT_WRAPPER();
|
|
}
|
|
|
|
bool cConnectionVTP::CmdDELC(const char *Option)
|
|
{
|
|
INIT_WRAPPER();
|
|
if (*Option) {
|
|
if (isnumber(Option)) {
|
|
if (!Channels.BeingEdited()) {
|
|
cChannel *channel = Channels.GetByNumber(strtol(Option, NULL, 10));
|
|
if (channel) {
|
|
for (cTimer *timer = Timers.First(); timer; timer = Timers.Next(timer)) {
|
|
if (timer->Channel() == channel) {
|
|
Reply(550, "Channel \"%s\" is in use by timer %d", Option, timer->Index() + 1);
|
|
return false;
|
|
}
|
|
}
|
|
int CurrentChannelNr = cDevice::CurrentChannel();
|
|
cChannel *CurrentChannel = Channels.GetByNumber(CurrentChannelNr);
|
|
if (CurrentChannel && channel == CurrentChannel) {
|
|
int n = Channels.GetNextNormal(CurrentChannel->Index());
|
|
if (n < 0)
|
|
n = Channels.GetPrevNormal(CurrentChannel->Index());
|
|
CurrentChannel = Channels.Get(n);
|
|
CurrentChannelNr = 0; // triggers channel switch below
|
|
}
|
|
Channels.Del(channel);
|
|
Channels.ReNumber();
|
|
Channels.SetModified(true);
|
|
isyslog("channel %s deleted", Option);
|
|
if (CurrentChannel && CurrentChannel->Number() != CurrentChannelNr) {
|
|
if (!cDevice::PrimaryDevice()->Replaying() || cDevice::PrimaryDevice()->Transferring())
|
|
Channels.SwitchTo(CurrentChannel->Number());
|
|
else
|
|
cDevice::SetCurrentChannel(CurrentChannel);
|
|
}
|
|
Reply(250, "Channel \"%s\" deleted", Option);
|
|
}
|
|
else
|
|
Reply(501, "Channel \"%s\" not defined", Option);
|
|
}
|
|
else
|
|
Reply(550, "Channels are being edited - try again later");
|
|
}
|
|
else
|
|
Reply(501, "Error in channel number \"%s\"", Option);
|
|
}
|
|
else {
|
|
Reply(501, "Missing channel number");
|
|
}
|
|
EXIT_WRAPPER();
|
|
}
|
|
|
|
bool cConnectionVTP::CmdDELR(const char *Option)
|
|
{
|
|
INIT_WRAPPER();
|
|
if (*Option) {
|
|
if (isnumber(Option)) {
|
|
cRecording *recording = Recordings.Get(strtol(Option, NULL, 10) - 1);
|
|
if (recording) {
|
|
cRecordControl *rc = cRecordControls::GetRecordControl(recording->FileName());
|
|
if (!rc) {
|
|
if (recording->Delete()) {
|
|
Reply(250, "Recording \"%s\" deleted", Option);
|
|
::Recordings.DelByName(recording->FileName());
|
|
}
|
|
else
|
|
Reply(554, "Error while deleting recording!");
|
|
}
|
|
else
|
|
Reply(550, "Recording \"%s\" is in use by timer %d", Option, rc->Timer()->Index() + 1);
|
|
}
|
|
else
|
|
Reply(550, "Recording \"%s\" not found%s", Option, Recordings.Count() ? "" : " (use LSTR before deleting)");
|
|
}
|
|
else
|
|
Reply(501, "Error in recording number \"%s\"", Option);
|
|
}
|
|
else
|
|
Reply(501, "Missing recording number");
|
|
EXIT_WRAPPER();
|
|
}
|
|
|
|
bool cConnectionVTP::CmdRENR(const char *Option)
|
|
{
|
|
INIT_WRAPPER();
|
|
#if defined(LIEMIKUUTIO) && LIEMIKUUTIO < 132
|
|
bool recordings = Recordings.Update(true);
|
|
if (recordings) {
|
|
if (*Option) {
|
|
char *tail;
|
|
int n = strtol(Option, &tail, 10);
|
|
cRecording *recording = Recordings.Get(n - 1);
|
|
if (recording && tail && tail != Option) {
|
|
#if APIVERSNUM < 10704
|
|
int priority = recording->priority;
|
|
int lifetime = recording->lifetime;
|
|
#endif
|
|
char *oldName = strdup(recording->Name());
|
|
tail = skipspace(tail);
|
|
#if APIVERSNUM < 10704
|
|
if (recording->Rename(tail, &priority, &lifetime)) {
|
|
#else
|
|
if (recording->Rename(tail)) {
|
|
#endif
|
|
Reply(250, "Renamed \"%s\" to \"%s\"", oldName, recording->Name());
|
|
Recordings.ChangeState();
|
|
Recordings.TouchUpdate();
|
|
}
|
|
else {
|
|
Reply(501, "Renaming \"%s\" to \"%s\" failed", oldName, tail);
|
|
}
|
|
free(oldName);
|
|
}
|
|
else {
|
|
Reply(501, "Recording not found or wrong syntax");
|
|
}
|
|
}
|
|
else {
|
|
Reply(501, "Missing Input settings");
|
|
}
|
|
}
|
|
else {
|
|
Reply(550, "No recordings available");
|
|
}
|
|
#else
|
|
Reply(501, "Rename not supported, please use LIEMIKUUTIO < 1.32");
|
|
#endif /* LIEMIKUUTIO */
|
|
EXIT_WRAPPER();
|
|
}
|
|
|
|
bool cConnectionVTP::Respond(int Code, const char *Message, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, Message);
|
|
cString str = cString::sprintf(Message, ap);
|
|
va_end(ap);
|
|
|
|
if (Code >= 0 && m_LastCommand != NULL) {
|
|
free(m_LastCommand);
|
|
m_LastCommand = NULL;
|
|
}
|
|
|
|
return cServerConnection::Respond("%03d%c%s", Code >= 0,
|
|
Code < 0 ? -Code : Code,
|
|
Code < 0 ? '-' : ' ', *str);
|
|
}
|
|
|
|
cString cConnectionVTP::ToText() const
|
|
{
|
|
cString str = cServerConnection::ToText();
|
|
if (m_LiveStreamer)
|
|
return cString::sprintf("%s\t%s", *str, *m_LiveStreamer->ToText());
|
|
else if (m_RecPlayer)
|
|
return cString::sprintf("%s\t%s", *str, m_RecPlayer->getCurrentRecording()->Name());
|
|
else
|
|
return str;
|
|
}
|