Delete unused script which records and converts at the same time.
[recordtv.git] / src / rtv_schedule.py
blob37510be60f362132346498e595f20ac190fca228
1 #!/usr/bin/python
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
10 # - logs
11 # - selections
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()
22 if pi1Pri > pi2Pri:
23 return -1
24 elif pi1Pri < pi2Pri:
25 return 1
26 elif pi1.startTime < pi2.startTime:
27 return -1
28 elif pi1.startTime > pi2.startTime:
29 return 1
31 return 0
33 class Schedule:
35 def __init__( self, config ):
36 self.config = config
38 def get_at_job( self, at_output ):
39 for ln in at_output:
40 m = at_output_re.match( ln )
41 if m:
42 return m.group( 1 )
43 print ( "Unable to understand at command output '%s' - "
44 + "can't create a scheduled_events entry" ) % at_output
45 return None
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).")
50 % ( losePi.title,
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"
59 + " (%s).")
60 % ( losePi.title,
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"
71 + " randomly.")
72 % ( losePi.title,
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 )
80 new_queue = []
82 for pi1Num in range( len( self.record_queue ) ):
83 pi1 = self.record_queue[pi1Num]
85 clashed_with = None
87 for pi2Num in range( pi1Num ):
88 pi2 = self.record_queue[pi2Num]
90 if pi1.clashes_with( pi2 ):
91 clashed_with = pi2
92 break
94 if not clashed_with:
95 new_queue.append( pi1 )
96 else:
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 )
101 else:
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."
110 return
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,
133 filename )
134 cmds_array = ( "at",
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(
160 pi.channel )
161 if not pi.channel_pretty:
162 print (
163 "Pretty channel name not found for channel %s"
164 % pi.channel )
165 pi.priority = fav.priority
166 self.record_queue.append( pi )
167 break
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()
177 pi.load( full_fn )
179 at_job_start = pi.atJob
180 done_atrm = False
181 if num_re.match( at_job_start ):
182 rtv_utils.run_command( ( "atrm", at_job_start ) )
183 done_atrm = True
185 if done_atrm:
186 os.unlink( full_fn )
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()
202 pi.load( infofn )
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
210 os.unlink( full_fn )
211 os.unlink( infofn )
215 def schedule( self, xmltv_parser = rtv_utils, fav_reader = rtv_selection,
216 scheduler = None ):
218 if scheduler == None:
219 scheduler = self
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 )
243 sch.schedule()
249 # === Test code ===
251 def format_pi_list( qu ):
252 ret = "[ "
253 for pi in qu[:-1]:
254 ret += pi.title
255 ret += ", "
256 if len( qu ) > 0:
257 ret += qu[-1].title
258 ret += " ]"
260 return ret
262 class FakeScheduler:
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,
290 minutes = 30 )
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,
298 minutes = 30 )
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,
304 minutes = 25 )
305 self.piLost.endTime = dtnow + datetime.timedelta( hours = 2,
306 minutes = 30 )
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,
312 minutes = 30 )
313 self.piTurnip.endTime = dtnow + datetime.timedelta( hours = 2,
314 minutes = 30 )
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 ):
342 self.queue = queue
344 def test( self ):
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"
379 + " looks like %s" )
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 ):
387 p = FakeScheduler()
388 p.schedule = Schedule( config )
390 p.test()