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
]
87 for pi2Num
in range( pi1Num
):
88 pi2
= self
.record_queue
[pi2Num
]
90 if pi1
.clashes_with( pi2
):
95 new_queue
.append( pi1
)
97 if pi1
.get_priority() < clashed_with
.get_priority():
98 self
.print_clash_priority_error( pi1
, clashed_with
)
99 elif pi1
.startTime
> clashed_with
.startTime
:
100 self
.print_clash_time_error( pi1
, clashed_with
)
102 self
.print_clash_same_time( pi1
, clashed_with
)
104 self
.record_queue
= new_queue
106 def schedule_recordings( self
, queue
):
108 if len( queue
) == 0:
109 print "No programmes found to record."
112 rtv_utils
.ensure_dir_exists( self
.config
.recorded_progs_dir
)
113 rtv_utils
.ensure_dir_exists( self
.config
.scheduled_events_dir
)
114 rtv_utils
.ensure_dir_exists( self
.config
.recording_log_dir
)
116 for programmeInfo
in queue
:
118 print "Recording '%s' at '%s'" % ( programmeInfo
.title
,
119 programmeInfo
.startTime
.strftime( MESSAGE_TIME_FORMAT
) )
121 filename
= rtv_utils
.prepare_filename( programmeInfo
.title
)
122 filename
+= programmeInfo
.startTime
.strftime( "-%Y-%m-%d_%H_%M" )
124 length_timedelta
= programmeInfo
.endTime
- programmeInfo
.startTime
125 length_in_seconds
= ( ( length_timedelta
.days
126 * rtv_utils
.SECS_IN_DAY
) + length_timedelta
.seconds
)
127 length_in_seconds
+= 60 * self
.config
.extra_recording_time_mins
129 sched_filename
= os
.path
.join( self
.config
.scheduled_events_dir
,
130 filename
+ ".rtvinfo" )
132 outfilename
= os
.path
.join( self
.config
.recorded_progs_dir
,
135 programmeInfo
.startTime
.strftime( "%H:%M %d.%m.%Y" ) )
136 at_output
= rtv_utils
.run_command_feed_input( cmds_array
,
137 self
.config
.record_start_command
% (
138 self
.channel_xmltv2tzap
.get_value( programmeInfo
.channel
),
139 outfilename
, length_in_seconds
, sched_filename
,
140 os
.path
.join( self
.config
.recording_log_dir
,
141 filename
+ ".log" ) ) )
142 at_job_start
= self
.get_at_job( at_output
)
144 if at_job_start
!= None:
145 programmeInfo
.atJob
= at_job_start
146 programmeInfo
.save( sched_filename
)
148 def sax_callback( self
, pi
):
149 for fav
in self
.favs_and_sels
:
150 if fav
.matches( pi
):
151 dtnow
= datetime
.datetime
.today()
152 if( pi
.startTime
> dtnow
153 and (pi
.startTime
- dtnow
).days
< 1 ):
155 if fav
.deleteAfterDays
:
156 pi
.deleteTime
= pi
.endTime
+ datetime
.timedelta(
157 float( fav
.deleteAfterDays
), 0 )
159 pi
.channel_pretty
= self
.channel_xmltv2tzap
.get_value(
161 if not pi
.channel_pretty
:
163 "Pretty channel name not found for channel %s"
165 pi
.priority
= fav
.priority
166 self
.record_queue
.append( pi
)
169 def remove_scheduled_events( self
):
171 rtv_utils
.ensure_dir_exists( self
.config
.scheduled_events_dir
)
173 for fn
in os
.listdir( self
.config
.scheduled_events_dir
):
174 full_fn
= os
.path
.join( self
.config
.scheduled_events_dir
, fn
)
176 pi
= rtv_programmeinfo
.ProgrammeInfo()
179 at_job_start
= pi
.atJob
181 if num_re
.match( at_job_start
):
182 rtv_utils
.run_command( ( "atrm", at_job_start
) )
188 def delete_pending_recordings( self
):
189 rtv_utils
.ensure_dir_exists( self
.config
.recorded_progs_dir
)
191 dttoday
= datetime
.datetime
.today()
193 for fn
in os
.listdir( self
.config
.recorded_progs_dir
):
194 if fn
[-4:] == ".flv":
196 infofn
= os
.path
.join( self
.config
.recorded_progs_dir
,
197 fn
[:-4] + ".rtvinfo" )
199 if os
.path
.isfile( infofn
):
201 pi
= rtv_programmeinfo
.ProgrammeInfo()
204 if pi
.deleteTime
and pi
.deleteTime
< dttoday
:
205 full_fn
= os
.path
.join(
206 self
.config
.recorded_progs_dir
, fn
)
208 print "Deleting file '%s'" % full_fn
215 def schedule( self
, xmltv_parser
= rtv_utils
, fav_reader
= rtv_selection
,
218 if scheduler
== None:
221 # TODO: if we are generating a TV guide as well as scheduling,
222 # we should share the same XML parser.
224 self
.delete_pending_recordings()
226 self
.favs_and_sels
= fav_reader
.read_favs_and_selections(
227 self
.config
, record_only
= True )
229 self
.channel_xmltv2tzap
= rtv_propertiesfile
.PropertiesFile()
230 self
.channel_xmltv2tzap
.load( self
.config
.channel_xmltv2tzap_file
)
232 scheduler
.remove_scheduled_events()
234 self
.record_queue
= []
235 xmltv_parser
.parse_xmltv_files( self
.config
, self
.sax_callback
)
236 self
.remove_clashes()
237 scheduler
.schedule_recordings( self
.record_queue
)
238 self
.record_queue
= []
241 def schedule( config
):
242 sch
= Schedule( config
)
251 def format_pi_list( qu
):
264 def __init__( self
):
266 self
.remove_scheduled_events_called
= False
268 dtnow
= datetime
.datetime
.today()
270 self
.favHeroes
= rtv_favourite
.Favourite()
271 self
.favHeroes
.title_re
= "Heroes"
273 self
.favNewsnight
= rtv_favourite
.Favourite()
274 self
.favNewsnight
.title_re
= "Newsnight.*"
275 self
.favNewsnight
.priority
= -50
277 self
.favLost
= rtv_favourite
.Favourite()
278 self
.favLost
.title_re
= "Lost"
279 self
.favLost
.priority
= -50
281 self
.piHeroes
= rtv_programmeinfo
.ProgrammeInfo()
282 self
.piHeroes
.title
= "Heroes"
283 self
.piHeroes
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1 )
284 self
.piHeroes
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2 )
285 self
.piHeroes
.channel
= "south-east.bbc2.bbc.co.uk"
287 self
.piNewsnight
= rtv_programmeinfo
.ProgrammeInfo()
288 self
.piNewsnight
.title
= "Newsnight"
289 self
.piNewsnight
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1,
291 self
.piNewsnight
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2 )
292 self
.piNewsnight
.channel
= "south-east.bbc1.bbc.co.uk"
294 self
.piNR
= rtv_programmeinfo
.ProgrammeInfo()
295 self
.piNR
.title
= "Newsnight Review"
296 self
.piNR
.startTime
= dtnow
+ datetime
.timedelta( hours
= 2 )
297 self
.piNR
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2,
299 self
.piNR
.channel
= "south-east.bbc2.bbc.co.uk"
301 self
.piLost
= rtv_programmeinfo
.ProgrammeInfo()
302 self
.piLost
.title
= "Lost"
303 self
.piLost
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1,
305 self
.piLost
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2,
307 self
.piLost
.channel
= "channel4.com"
309 self
.piTurnip
= rtv_programmeinfo
.ProgrammeInfo()
310 self
.piTurnip
.title
= "Newsnight Turnip"
311 self
.piTurnip
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1,
313 self
.piTurnip
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2,
315 self
.piTurnip
.channel
= "channel5.co.uk"
317 self
.which_test
= None
319 def parse_xmltv_files( self
, config
, callback
):
320 if self
.which_test
== "no_clash1":
321 callback( self
.piNewsnight
)
322 callback( self
.piNR
)
323 elif self
.which_test
== "priority_clash1":
324 callback( self
.piHeroes
)
325 callback( self
.piNewsnight
)
326 elif self
.which_test
== "time_clash1":
327 callback( self
.piNewsnight
)
328 callback( self
.piLost
)
329 elif self
.which_test
== "same_clash1":
330 callback( self
.piNewsnight
)
331 callback( self
.piTurnip
)
333 def remove_scheduled_events( self
):
334 if self
.remove_scheduled_events_called
:
335 raise Exception( "remove_scheduled_events called twice." )
336 self
.remove_scheduled_events_called
= True
338 def read_favs_and_selections( self
, config
):
339 return [ self
.favHeroes
, self
.favNewsnight
, self
.favLost
]
341 def schedule_recordings( self
, queue
):
345 self
.which_test
= "priority_clash1"
346 self
.schedule
.schedule( self
, self
, self
)
347 if not self
.remove_scheduled_events_called
:
348 raise Exception( "remove_scheduled_events never called" )
349 if self
.queue
!= [self
.piHeroes
]:
350 raise Exception( "queue should look like %s, but it looks like %s"
351 % ( [self
.piHeroes
], self
.queue
) )
354 self
.which_test
= "no_clash1"
355 self
.remove_scheduled_events_called
= False
356 self
.schedule
.schedule( self
, self
, self
)
357 if not self
.remove_scheduled_events_called
:
358 raise Exception( "remove_scheduled_events never called" )
359 if self
.queue
!= [self
.piNewsnight
, self
.piNR
]:
360 raise Exception( "queue should look like %s, but it looks like %s"
361 % ( [self
.piNewsnight
, self
.piNR
], self
.queue
) )
363 self
.which_test
= "time_clash1"
364 self
.remove_scheduled_events_called
= False
365 self
.schedule
.schedule( self
, self
, self
)
366 if not self
.remove_scheduled_events_called
:
367 raise Exception( "remove_scheduled_events never called" )
368 if self
.queue
!= [self
.piLost
]:
369 raise Exception( "queue should look like %s, but it looks like %s"
370 % ( [self
.piLost
], self
.queue
) )
372 self
.which_test
= "same_clash1"
373 self
.remove_scheduled_events_called
= False
374 self
.schedule
.schedule( self
, self
, self
)
375 if not self
.remove_scheduled_events_called
:
376 raise Exception( "remove_scheduled_events never called" )
377 if self
.queue
!= [self
.piNewsnight
] and self
.queue
!= [self
.piTurnip
]:
378 raise Exception( ("queue should look like %s or %s, but it"
380 % ( format_pi_list( [self
.piNewsnight
] ),
381 format_pi_list( [self
.piTurnip
] ),
382 format_pi_list( self
.queue
) ) )
384 print "test_schedule Passed"
386 def test_schedule( config
):
388 p
.schedule
= Schedule( config
)