3 import time
, datetime
, os
, re
4 import rtv_favourite
, rtv_utils
, rtv_programmeinfo
5 import rtv_propertiesfile
, rtv_selection
7 # TODO: prevent overlapping recordings
8 # TODO: delete old files:
9 # - scheduled recordings
13 MESSAGE_TIME_FORMAT
= "%H:%M on %a"
15 at_output_re
= re
.compile( "job (\d+) at .*\n" )
16 num_re
= re
.compile( "\d+" )
18 def priority_time_compare( pi1
, pi2
):
19 pi1Pri
= pi1
.get_priority()
20 pi2Pri
= pi2
.get_priority()
26 elif pi1
.startTime
< pi2
.startTime
:
28 elif pi1
.startTime
> pi2
.startTime
:
35 def __init__( self
, config
):
38 def get_at_job( self
, at_output
):
40 m
= at_output_re
.match( ln
)
43 print ( "Unable to understand at command output '%s' - "
44 + "can't create a scheduled_events entry" ) % at_output
47 def print_clash_priority_error( self
, losePi
, keepPi
):
48 print ( ("Not recording '%s' at %s - it clashes with '%s',"
49 + " which is higher priority (%d > %d).")
51 losePi
.startTime
.strftime(
52 MESSAGE_TIME_FORMAT
),
53 keepPi
.title
, keepPi
.get_priority(),
54 losePi
.get_priority() ) )
56 def print_clash_time_error( self
, losePi
, keepPi
):
57 print ( ("Not recording '%s' at %s - it clashes with '%s',"
58 + " which has the same priority (%d), but starts earlier"
61 losePi
.startTime
.strftime(
62 MESSAGE_TIME_FORMAT
),
63 keepPi
.title
, keepPi
.get_priority(),
64 keepPi
.startTime
.strftime(
65 MESSAGE_TIME_FORMAT
) ) )
67 def print_clash_same_time( self
, losePi
, keepPi
):
68 print ( ("Not recording '%s' at %s - it clashes with '%s',"
69 + " which has the same priority (%d). They start at"
70 + " the same time, so the one to record was chosen"
73 losePi
.startTime
.strftime(
74 MESSAGE_TIME_FORMAT
),
75 keepPi
.title
, keepPi
.get_priority() ) )
78 def remove_clashes( self
):
79 self
.record_queue
.sort( priority_time_compare
)
82 for pi1Num
in range( len( self
.record_queue
) ):
83 pi1
= self
.record_queue
[pi1Num
]
89 if pi1
.clashes_with( pi2
):
94 new_queue
.append( pi1
)
96 if pi1
.get_priority() < clashed_with
.get_priority():
97 self
.print_clash_priority_error( pi1
, clashed_with
)
98 elif pi1
.startTime
> clashed_with
.startTime
:
99 self
.print_clash_time_error( pi1
, clashed_with
)
101 self
.print_clash_same_time( pi1
, clashed_with
)
103 self
.record_queue
= new_queue
105 def schedule_recordings( self
, queue
):
107 if len( queue
) == 0:
108 print "No programmes found to record."
111 rtv_utils
.ensure_dir_exists( self
.config
.recorded_progs_dir
)
112 rtv_utils
.ensure_dir_exists( self
.config
.scheduled_events_dir
)
113 rtv_utils
.ensure_dir_exists( self
.config
.recording_log_dir
)
115 for programmeInfo
in queue
:
117 print "Recording '%s' at '%s'" % ( programmeInfo
.title
,
118 programmeInfo
.startTime
.strftime( MESSAGE_TIME_FORMAT
) )
120 filename
= rtv_utils
.prepare_filename( programmeInfo
.title
)
121 filename
+= programmeInfo
.startTime
.strftime( "-%Y-%m-%d_%H_%M" )
123 length_timedelta
= programmeInfo
.endTime
- programmeInfo
.startTime
124 length_in_seconds
= ( ( length_timedelta
.days
125 * rtv_utils
.SECS_IN_DAY
) + length_timedelta
.seconds
)
126 length_in_seconds
+= 60 * self
.config
.extra_recording_time_mins
128 sched_filename
= os
.path
.join( self
.config
.scheduled_events_dir
,
129 filename
+ ".rtvinfo" )
131 outfilename
= os
.path
.join( self
.config
.recorded_progs_dir
,
134 programmeInfo
.startTime
.strftime( "%H:%M %d.%m.%Y" ) )
135 at_output
= rtv_utils
.run_command_feed_input( cmds_array
,
136 self
.config
.record_start_command
% (
137 self
.channel_xmltv2tzap
.get_value( programmeInfo
.channel
),
138 outfilename
, length_in_seconds
, sched_filename
,
139 os
.path
.join( self
.config
.recording_log_dir
,
140 filename
+ ".log" ) ) )
141 at_job_start
= self
.get_at_job( at_output
)
143 if at_job_start
!= None:
144 programmeInfo
.atJob
= at_job_start
145 programmeInfo
.save( sched_filename
)
147 def sax_callback( self
, pi
):
148 for fav
in self
.favs_and_sels
:
149 if fav
.matches( pi
):
150 dtnow
= datetime
.datetime
.today()
151 if( pi
.startTime
> dtnow
152 and (pi
.startTime
- dtnow
).days
< 1 ):
154 if fav
.deleteAfterDays
:
155 pi
.deleteTime
= pi
.endTime
+ datetime
.timedelta(
156 float( fav
.deleteAfterDays
), 0 )
158 pi
.channel_pretty
= self
.channel_xmltv2tzap
.get_value(
160 if not pi
.channel_pretty
:
162 "Pretty channel name not found for channel %s"
164 pi
.priority
= fav
.priority
165 self
.record_queue
.append( pi
)
168 def remove_scheduled_events( self
):
170 rtv_utils
.ensure_dir_exists( self
.config
.scheduled_events_dir
)
172 for fn
in os
.listdir( self
.config
.scheduled_events_dir
):
173 full_fn
= os
.path
.join( self
.config
.scheduled_events_dir
, fn
)
175 pi
= rtv_programmeinfo
.ProgrammeInfo()
178 at_job_start
= pi
.atJob
180 if num_re
.match( at_job_start
):
181 rtv_utils
.run_command( ( "atrm", at_job_start
) )
187 def delete_pending_recordings( self
):
188 rtv_utils
.ensure_dir_exists( self
.config
.recorded_progs_dir
)
190 dttoday
= datetime
.datetime
.today()
192 for fn
in os
.listdir( self
.config
.recorded_progs_dir
):
193 if fn
[-4:] == ".flv":
195 infofn
= os
.path
.join( self
.config
.recorded_progs_dir
,
196 fn
[:-4] + ".rtvinfo" )
198 if os
.path
.isfile( infofn
):
200 pi
= rtv_programmeinfo
.ProgrammeInfo()
203 if pi
.deleteTime
and pi
.deleteTime
< dttoday
:
204 full_fn
= os
.path
.join(
205 self
.config
.recorded_progs_dir
, fn
)
207 print "Deleting file '%s'" % full_fn
214 def schedule( self
, xmltv_parser
= rtv_utils
, fav_reader
= rtv_selection
,
217 if scheduler
== None:
220 # TODO: if we are generating a TV guide as well as scheduling,
221 # we should share the same XML parser.
223 self
.delete_pending_recordings()
225 self
.favs_and_sels
= fav_reader
.read_favs_and_selections(
226 self
.config
, record_only
= True )
228 self
.channel_xmltv2tzap
= rtv_propertiesfile
.PropertiesFile()
229 self
.channel_xmltv2tzap
.load( self
.config
.channel_xmltv2tzap_file
)
231 scheduler
.remove_scheduled_events()
233 self
.record_queue
= []
234 xmltv_parser
.parse_xmltv_files( self
.config
, self
.sax_callback
)
235 self
.remove_clashes()
236 scheduler
.schedule_recordings( self
.record_queue
)
237 self
.record_queue
= []
240 def schedule( config
):
241 sch
= Schedule( config
)
250 def format_pi_list( qu
):
263 def __init__( self
):
265 self
.remove_scheduled_events_called
= False
267 dtnow
= datetime
.datetime
.today()
269 self
.favHeroes
= rtv_favourite
.Favourite()
270 self
.favHeroes
.title_re
= "Heroes"
272 self
.favNewsnight
= rtv_favourite
.Favourite()
273 self
.favNewsnight
.title_re
= "Newsnight.*"
274 self
.favNewsnight
.priority
= -50
276 self
.favLost
= rtv_favourite
.Favourite()
277 self
.favLost
.title_re
= "Lost"
278 self
.favLost
.priority
= -50
280 self
.piHeroes
= rtv_programmeinfo
.ProgrammeInfo()
281 self
.piHeroes
.title
= "Heroes"
282 self
.piHeroes
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1 )
283 self
.piHeroes
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2 )
284 self
.piHeroes
.channel
= "south-east.bbc2.bbc.co.uk"
286 self
.piNewsnight
= rtv_programmeinfo
.ProgrammeInfo()
287 self
.piNewsnight
.title
= "Newsnight"
288 self
.piNewsnight
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1,
290 self
.piNewsnight
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2 )
291 self
.piNewsnight
.channel
= "south-east.bbc1.bbc.co.uk"
293 self
.piNR
= rtv_programmeinfo
.ProgrammeInfo()
294 self
.piNR
.title
= "Newsnight Review"
295 self
.piNR
.startTime
= dtnow
+ datetime
.timedelta( hours
= 2 )
296 self
.piNR
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2,
298 self
.piNR
.channel
= "south-east.bbc2.bbc.co.uk"
300 self
.piLost
= rtv_programmeinfo
.ProgrammeInfo()
301 self
.piLost
.title
= "Lost"
302 self
.piLost
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1,
304 self
.piLost
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2,
306 self
.piLost
.channel
= "channel4.com"
308 self
.piTurnip
= rtv_programmeinfo
.ProgrammeInfo()
309 self
.piTurnip
.title
= "Newsnight Turnip"
310 self
.piTurnip
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1,
312 self
.piTurnip
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2,
314 self
.piTurnip
.channel
= "channel5.co.uk"
316 self
.which_test
= None
318 def parse_xmltv_files( self
, config
, callback
):
319 if self
.which_test
== "no_clash1":
320 callback( self
.piNewsnight
)
321 callback( self
.piNR
)
322 elif self
.which_test
== "priority_clash1":
323 callback( self
.piHeroes
)
324 callback( self
.piNewsnight
)
325 elif self
.which_test
== "time_clash1":
326 callback( self
.piNewsnight
)
327 callback( self
.piLost
)
328 elif self
.which_test
== "same_clash1":
329 callback( self
.piNewsnight
)
330 callback( self
.piTurnip
)
332 def remove_scheduled_events( self
):
333 if self
.remove_scheduled_events_called
:
334 raise Exception( "remove_scheduled_events called twice." )
335 self
.remove_scheduled_events_called
= True
337 def read_favs_and_selections( self
, config
):
338 return [ self
.favHeroes
, self
.favNewsnight
, self
.favLost
]
340 def schedule_recordings( self
, queue
):
344 self
.which_test
= "priority_clash1"
345 self
.schedule
.schedule( self
, self
, self
)
346 if not self
.remove_scheduled_events_called
:
347 raise Exception( "remove_scheduled_events never called" )
348 if self
.queue
!= [self
.piHeroes
]:
349 raise Exception( "queue should look like %s, but it looks like %s"
350 % ( [self
.piHeroes
], self
.queue
) )
353 self
.which_test
= "no_clash1"
354 self
.remove_scheduled_events_called
= False
355 self
.schedule
.schedule( self
, self
, self
)
356 if not self
.remove_scheduled_events_called
:
357 raise Exception( "remove_scheduled_events never called" )
358 if self
.queue
!= [self
.piNewsnight
, self
.piNR
]:
359 raise Exception( "queue should look like %s, but it looks like %s"
360 % ( [self
.piNewsnight
, self
.piNR
], self
.queue
) )
362 self
.which_test
= "time_clash1"
363 self
.remove_scheduled_events_called
= False
364 self
.schedule
.schedule( self
, self
, self
)
365 if not self
.remove_scheduled_events_called
:
366 raise Exception( "remove_scheduled_events never called" )
367 if self
.queue
!= [self
.piLost
]:
368 raise Exception( "queue should look like %s, but it looks like %s"
369 % ( [self
.piLost
], self
.queue
) )
371 self
.which_test
= "same_clash1"
372 self
.remove_scheduled_events_called
= False
373 self
.schedule
.schedule( self
, self
, self
)
374 if not self
.remove_scheduled_events_called
:
375 raise Exception( "remove_scheduled_events never called" )
376 if self
.queue
!= [self
.piNewsnight
] and self
.queue
!= [self
.piTurnip
]:
377 raise Exception( ("queue should look like %s or %s, but it"
379 % ( format_pi_list( [self
.piNewsnight
] ),
380 format_pi_list( [self
.piTurnip
] ),
381 format_pi_list( self
.queue
) ) )
383 print "test_schedule Passed"
385 def test_schedule( config
):
387 p
.schedule
= Schedule( config
)