3
#include <vdr/plugin.h>
4
#include <vdr/channels.h>
6
#include <vdr/config.h>
7
#include <vdr/device.h>
9
#include "livefeatures.h"
13
#include "epg_events.h"
17
using namespace vdrlive;
21
string short_description;
23
string description_trunc;
34
std::string channel_groups_setting;
35
std::vector<std::string> channel_groups_names;
36
std::vector< std::vector<int> > channel_groups_numbers;
37
std::vector<std::string> times_names;
38
std::vector<time_t> times_start;
42
unsigned int time_para = 0;
44
<%session scope="global">
45
bool logged_in(false);
47
<%request scope="page">
48
unsigned int channel_group=0;
49
unsigned int time_selected=0;
51
<%include>page_init.eh</%include>
53
if (!logged_in && LiveSetup().UseAuth()) return reply.redirect("login.html");
54
pageTitle = tr("MultiSchedule");
56
ReadLock channelsLock( Channels );
58
throw HtmlError( tr("Couldn't aquire access to channels, please try again later.") );
60
#define MAX_CHANNELS 10
63
#define MINUTES_PER_ROW 5
64
#define CHARACTERS_PER_ROW 30
66
if ( ( channel_groups_setting.compare(LiveSetup().GetChannelGroups()) != 0 ) || ( channel_groups_numbers.size() == 0 ) )
68
// build the groups of channels to display
69
std::string channelGroups=LiveSetup().GetChannelGroups();
70
if ( channelGroups.empty() )
72
// setup default channel groups
73
int lastChannel = LiveSetup().GetLastChannel();
74
if ( lastChannel == 0 )
75
lastChannel = Channels.MaxNumber();
76
std::stringstream groups;
78
for (cChannel *channel = Channels.First(); channel && (channel->Number() <= lastChannel); channel = Channels.Next(channel))
80
if (channel->GroupSep())
82
groups << channel->Number();
88
channelGroups = groups.str();
89
LiveSetup().SetChannelGroups( channelGroups );
91
channel_groups_names.clear();
92
channel_groups_numbers.clear();
93
channel_groups_setting = channelGroups;
95
std::string thisGroup = "";
96
while ( ! channelGroups.empty() )
98
groupSep = channelGroups.find(';');
99
thisGroup = channelGroups.substr(0, groupSep );
100
if ( groupSep != channelGroups.npos )
101
channelGroups.erase(0, groupSep+1 );
105
int cur_group_count=0;
106
channel_groups_names.push_back( std::string() );
107
channel_groups_numbers.push_back( std::vector<int>() );
108
while ( !thisGroup.empty() )
110
std::string thisChannel;
112
if ( cur_group_count != 0 )
113
channel_groups_names.back() += std::string( " - " );
114
size_t channelSep = thisGroup.find(',');
115
thisChannel = thisGroup.substr(0, channelSep );
116
if ( channelSep != thisGroup.npos )
117
thisGroup.erase( 0, channelSep+1 );
120
int channel_no = lexical_cast< int > (thisChannel);
121
cChannel* Channel = Channels.GetByNumber( channel_no );
124
esyslog("Live: could not find channel no '%s'.", thisChannel.c_str() );
127
channel_groups_names.back() += std::string( Channel->Name() );
128
channel_groups_numbers.back().push_back( Channel->Number() );
130
if ( cur_group_count>=MAX_CHANNELS )
132
// start new group if group gets too large
134
channel_groups_names.push_back( std::string() );
135
channel_groups_numbers.push_back( std::vector<int>() );
138
catch ( const bad_lexical_cast & )
140
esyslog("Live: could not convert '%s' into a channel number", thisChannel.c_str());
147
if (cDevice::CurrentChannel()) {
148
// find group corresponding to current channel
150
int curChannel = cDevice::CurrentChannel();
151
for ( std::vector< std::vector<int> >::iterator grIt = channel_groups_numbers.begin();
152
grIt != channel_groups_numbers.end() && channel < 0; ++grIt, ++curGroup )
154
for ( std::vector<int>::iterator chIt = (*grIt).begin();
155
chIt != (*grIt).end() && channel < 0; ++ chIt )
157
if ( *chIt == curChannel )
158
channel_group = channel = curGroup;
161
// if nothing is found, fall back to group 0
166
channel_group = channel;
170
if ( channel >= (int)channel_groups_numbers.size() )
172
channel_group = channel;
178
// calculate time of midnight (localtime) and convert back to GMT
179
time_t now = (time(NULL)/3600)*3600;
180
time_t now_local = time(NULL);
182
if ( localtime_r( &now_local, &tm_r ) == 0 ) {
183
ostringstream builder;
184
builder << "cannot represent timestamp " << now_local << " as local time";
185
throw runtime_error( builder.str() );
190
time_t midnight = mktime( &tm_r );
192
// add four 8h steps per day to the time list
193
for (int i=0; i<4*MAX_DAYS ; i++ )
195
times_start.push_back( midnight + MAX_HOURS*3600*i );
197
vector< string > parts = StringSplit( LiveSetup().GetTimes(), ';' );
198
vector< time_t > offsets;
199
vector< string >::const_iterator part = parts.begin();
200
for ( ; part != parts.end(); ++part )
203
unsigned int sep = (*part).find(':');
204
std::string hour = (*part).substr(0, sep );
205
if ( sep == (*part).npos )
207
esyslog("Live: Error parsing time '%s'", (*part).c_str() );
210
std::string min = (*part).substr(sep+1, (*part).npos );
211
offsets.push_back( lexical_cast<time_t>( hour )*60*60 + lexical_cast<time_t>( min ) *60 );
213
catch ( const bad_lexical_cast & ) {
214
esyslog("Live: Error parsing time '%s'", part->c_str() );
217
// add the time of the favourites to the time list
218
for (int i=0; i< MAX_DAYS ; i++ )
220
vector< time_t >::const_iterator offset = offsets.begin();
221
for ( ; offset != offsets.end(); ++offset )
223
times_start.push_back( midnight + 24*3600*i + *offset );
227
times_start.push_back( now );
229
std::sort( times_start.begin(), times_start.end() );
230
// delete every time which has already passed
231
while ( *times_start.begin()< now )
232
times_start.erase(times_start.begin() );
234
// build the corresponding names
235
for ( vector< time_t >::const_iterator start = times_start.begin();
236
start != times_start.end(); ++start )
238
times_names.push_back(FormatDateTime( tr("%A, %x"), *start)
239
+std::string(" ")+ FormatDateTime( tr("%I:%M %p"), *start) );
241
// the first time is now
242
times_names[0]=tr("Now");
244
if ( time_para >= times_names.size() )
245
time_para = times_names.size()-1;
246
time_selected=time_para;
249
<& pageelems.doc_type &>
252
<title>VDR Live - <$ pageTitle $></title>
253
<& pageelems.stylesheets &>
254
<& pageelems.ajax_js &>
258
<& menu active=("multischedule") component=("multischedule.channel_selection") &>
261
cSchedulesLock schedulesLock;
262
cSchedules const* schedules = cSchedules::Schedules( schedulesLock );
264
time_t now = time(NULL);
265
if ( time_para >= times_start.size() )
266
time_para = times_start.size()-1;
267
time_t sched_start = (times_start[ time_para ]/300)*300;
270
max_hours = lexical_cast<time_t>( LiveSetup().GetScheduleDuration() );
272
catch ( const bad_lexical_cast & )
274
esyslog("Live: could not convert '%s' into a schedule duration", LiveSetup().GetScheduleDuration().c_str());
280
time_t sched_end = sched_start + 60 * 60 * max_hours;
281
int sched_end_row = ( sched_end - sched_start ) / 60 / MINUTES_PER_ROW;
282
std::list<SchedEntry> table[MAX_CHANNELS];
283
std::vector<std::string> channel_names(channel_groups_numbers[ channel ].size() );
284
std::vector<tChannelID> channel_IDs(channel_groups_numbers[ channel ].size() );
285
if ( channel >= (int)channel_groups_numbers.size() )
286
channel = channel_groups_numbers.size()-1;
287
//for ( int chan = 0; chan<MAX_CHANNELS; chan++)
288
for ( unsigned int j = 0; j<channel_groups_numbers[ channel ].size(); j++)
293
int chan = channel_groups_numbers[ channel ][ j ];
295
cChannel* Channel = Channels.GetByNumber( chan );
298
if ( Channel->GroupSep() || Channel->Name() == '\0' )
300
channel_names[ j ] = Channel->Name();
301
channel_IDs[ j ] = Channel->GetChannelID();
303
cSchedule const* Schedule = schedules->GetSchedule( Channel );
306
for (const cEvent *Event = Schedule->Events()->First(); Event;
307
Event = Schedule->Events()->Next(Event) )
309
if (Event->EndTime() <= sched_start )
311
if (Event->StartTime() >= sched_end )
314
EpgInfoPtr epgEvent = EpgEvents::CreateEpgInfo(Channel, Event);
315
if ( prev_row < 0 && Event->StartTime() > sched_start + MINUTES_PER_ROW )
317
// insert dummy event at start
318
table[ j ].push_back( SchedEntry() );
319
SchedEntry &en=table[ j ].back();
320
int event_start_row = (Event->StartTime() - sched_start) / 60 / MINUTES_PER_ROW;
322
en.row_count = event_start_row;
323
// no title and no start time = dummy event
326
prev_row = en.start_row + en.row_count;
328
table[ j ].push_back( SchedEntry() );
329
SchedEntry &en=table[j].back();
331
en.title = epgEvent->Title();
332
en.short_description = epgEvent->ShortDescr();
333
en.description = epgEvent->LongDescr();
334
en.start = epgEvent->StartTime(tr("%I:%M %p"));
335
en.end = epgEvent->EndTime(tr("%I:%M %p"));
336
en.day = epgEvent->StartTime(tr("%A, %b %d %Y"));
337
en.epgid = EpgEvents::EncodeDomId(Channel->GetChannelID(), Event->EventID());
338
en.has_timer = LiveTimerManager().GetTimer(Event->EventID(), Channel->GetChannelID() ) != 0;
340
en.start_row = prev_row > 0 ? prev_row : 0;
341
int end_time = Schedule->Events()->Next(Event) ?
342
Schedule->Events()->Next(Event)->StartTime() :
344
if (end_time > sched_end)
345
end_time = sched_end;
346
int next_event_start_row = (end_time - sched_start) / 60 / MINUTES_PER_ROW;
347
en.row_count = next_event_start_row - en.start_row;
348
if ( en.row_count < 1 )
350
prev_row = en.start_row + en.row_count;
352
// truncate description if too long
354
en.description_trunc=StringWordTruncate( en.description,
355
CHARACTERS_PER_ROW*(en.row_count-2),
361
if ( table[ j ].begin() == table[ j ].end() )
363
// no entries... create a single dummy entry
364
table[ j ].push_back( SchedEntry() );
365
SchedEntry &en=table[ j ].back();
367
en.row_count = sched_end_row;
368
// no title and no start time = dummy event
374
<table class="mschedule" cellspacing="0" cellpadding="0">
377
<tr class=" topaligned ">
378
<td > <div class="boxheader"> <div><div><$ tr("Time") $></div></div> </div></td>
379
<td class="time spacer"> </td>
381
for ( unsigned int channel = 0; channel< channel_names.size() ; channel++)
384
<td> <div class="boxheader"> <div> <div><$ channel_names[channel] $> <# reply.sout() automatically escapes special characters to html entities #>
385
<& pageelems.ajax_action_href action="switch_channel" tip=(tr("Switch to this channel.")) param=(channel_IDs[channel]) image="zap.png" alt="" &>
386
<& pageelems.vlc_stream_channel channelId=(channel_IDs[channel]) &>
387
</div></div> </div></td>
388
<td class="time spacer"> </td>
395
std::list<SchedEntry>::iterator cur_event[ MAX_CHANNELS ];
396
for (int i=0;i<MAX_CHANNELS;i++)
397
cur_event[i]=table[i].begin();
398
for (int row = 0 ; row < sched_end_row; row++ )
400
int minutes= ( (sched_start + row * 60 * MINUTES_PER_ROW ) % 3600 ) / 60;
402
if ( minutes < MINUTES_PER_ROW )
404
// full hour, swap odd/even
407
if ( (sched_start + row * 60 * MINUTES_PER_ROW ) <= now &&
408
(sched_start + (row+1) * 60 * MINUTES_PER_ROW ) > now )
410
row_class +=" current_row ";
412
row_class += odd ? " odd " : " even ";
416
<td class=" time leftcol rightcol <$ row_class $>">
418
if ( minutes < MINUTES_PER_ROW )
421
<$ FormatDateTime( tr("%I:%M %p"), sched_start + row * 60 * MINUTES_PER_ROW ) $>
433
for ( unsigned int channel = 0; channel< channel_names.size() ; channel++)
435
// output spacer column
437
<td class = " time spacer " > </td>
439
if ( cur_event[channel] == table[channel].end()
440
|| cur_event[channel]->start_row != row )
441
// no new event in this channel, skip it
444
SchedEntry &en=*cur_event[channel];
445
if (en.title.empty() && en.start.empty() )
449
<td class="event topaligned leftcol rightcol" rowspan="<$ en.row_count $>">
452
++cur_event[channel];
456
// output an event cell
458
<td class="event topaligned leftcol rightcol <$ en.has_timer ? "has_timer" : "" $>" rowspan="<$ en.row_count $>">
459
<div class=" content1 " >
460
<div class=" tools1 " >
461
<& pageelems.event_timer epgid=(en.epgid) &>
463
if (LiveFeatures<features::epgsearch>().Recent() ) {
465
<a href="searchresults.html?searchplain=<$ StringUrlEncode(en.title) $>"><img src="<$ LiveSetup().GetThemedLink("img", "search.png") $>" alt="" <& tooltip.hint text=(tr("Search for repeats.")) &>></img></a>
468
</%cpp><img src="img/transparent.png" width="16" height="16"><%cpp>
471
<& pageelems.imdb_info_href title=(en.title) &>
472
</div><div class= "start withmargin"><$ en.start $></div>
473
<div class="title withmargin"><a <& tooltip.hint text=(StringEscapeAndBreak(tr("Click to view details."))) &><& tooltip.display domId=en.epgid &>><$ en.title $></a></div>
475
if ( en.row_count>2 && !en.short_description.empty() )
478
<div class="short withmargin"><$ en.short_description.empty() ? " " : en.short_description $></div>
481
if ( en.row_count>3 && ! en.description_trunc.empty() )
484
<div class="description withmargin"><$en.description_trunc$>...
489
<a <& tooltip.hint text=(StringEscapeAndBreak(tr("Click to view details."))) &><& tooltip.display domId=en.epgid &>> <$ tr("more") $></a>
500
// move to next event for this channel
501
++cur_event[channel];
510
for ( unsigned int channel = 0; channel <= channel_names.size() ; channel++)
513
<td class = " event leftcol rightcol bottomrow " > </td>
514
<td class = " time spacer " > </td>
523
<%include>page_exit.eh</%include>
525
<%def channel_selection>
526
<form action="multischedule.html" method="get" id="channels">
528
<label for="channel"><$ tr("Channel") $>: <span class="bold"></span></label>
529
<select name="channel" id="channel" onchange="document.forms.channels.submit()" >
530
% for ( unsigned int i = 0; i < channel_groups_names.size(); ++i ) {
532
<option value="<$ i $>"
533
% if ( i == channel_group )
537
><$ channel_groups_names[i] $></option>
540
<label for="time_para"><$ tr("Time") $>: <span class="bold"></span></label>
541
<select name="time_para" id="time_para" onchange="document.forms.channels.submit()" >
542
% for ( unsigned int i = 0; i < times_names.size(); ++i ) {
544
<option value="<$ i $>"
545
% if ( i == time_selected )
549
><$ times_names[i] $></option>
552
% // <& pageelems.ajax_action_href action="switch_channel" tip=(tr("Switch to this channel.")) param=(Channel->GetChannelID()) image="zap.png" alt="" &>
553
% // <& pageelems.vlc_stream_channel channelId=(Channel->GetChannelID()) &>