3 import time
, datetime
, os
, re
4 import rtv_favourite
, rtv_utils
, rtv_programmeinfo
5 import rtv_propertiesfile
, rtv_selection
7 # TODO: delete old files:
8 # - scheduled recordings
12 MESSAGE_TIME_FORMAT
= "%H:%M on %a"
14 at_output_re
= re
.compile( "job (\d+) at .*\n" )
16 def priority_time_compare( pi1
, pi2
):
17 pi1Pri
= pi1
.get_priority()
18 pi2Pri
= pi2
.get_priority()
24 elif pi1
.startTime
< pi2
.startTime
:
26 elif pi1
.startTime
> pi2
.startTime
:
33 def __init__( self
, config
):
36 def add_to_old_progs_map( self
, dr
, fn
, ret
, delete_unused
):
37 if not fn
.endswith( ".rtvinfo" ):
40 full_path
= os
.path
.join( dr
, fn
)
41 proginfo
= rtv_programmeinfo
.ProgrammeInfo()
42 proginfo
.load( full_path
)
44 if proginfo
.sub_title
is None or proginfo
.unique_subtitles
== False:
46 os
.remove( full_path
)
49 if proginfo
.title
not in ret
:
50 ret
[proginfo
.title
] = {}
52 if proginfo
.sub_title
not in ret
[proginfo
.title
]:
53 ret
[proginfo
.title
][proginfo
.sub_title
] = 1
55 def find_old_programmes_map( self
, old_dir
, converted_dir
):
58 dr_list
= os
.listdir( old_dir
)
60 self
.add_to_old_progs_map( old_dir
, fn
, ret
, True )
62 for (dirpath
, dirnames
, filenames
) in os
.walk( converted_dir
):
64 self
.add_to_old_progs_map( dirpath
, fn
, ret
, False )
68 def get_at_job( self
, at_output
):
70 m
= at_output_re
.match( ln
)
73 print ( "Unable to understand at command output '%s' - "
74 + "can't create a scheduled_events entry" ) % at_output
77 def print_already_recorded( self
, prog
):
78 print ( "Not recording '%s : %s' - we have recorded it in the past."
79 % ( prog
.title
, prog
.sub_title
) )
81 def remove_already_recorded( self
, scheduler
):
82 old_dir
= os
.path
.join( self
.config
.recorded_progs_dir
, "old" )
83 converted_dir
= self
.config
.converted_progs_dir
84 old_progs_map
= scheduler
.find_old_programmes_map( old_dir
,
89 for prog
in self
.record_queue
:
90 if ( prog
.title
in old_progs_map
and
91 prog
.sub_title
in old_progs_map
[prog
.title
] ):
92 self
.print_already_recorded( prog
)
94 new_queue
.append( prog
)
96 self
.record_queue
= new_queue
99 def print_clash_priority_error( self
, losePi
, keepPi
):
100 print ( ("Not recording '%s' at %s - it clashes with '%s',"
101 + " which is higher priority (%d > %d).")
103 losePi
.startTime
.strftime(
104 MESSAGE_TIME_FORMAT
),
105 keepPi
.title
, keepPi
.get_priority(),
106 losePi
.get_priority() ) )
108 def print_clash_time_error( self
, losePi
, keepPi
):
109 print ( ("Not recording '%s' at %s - it clashes with '%s',"
110 + " which has the same priority (%d), but starts earlier"
113 losePi
.startTime
.strftime(
114 MESSAGE_TIME_FORMAT
),
115 keepPi
.title
, keepPi
.get_priority(),
116 keepPi
.startTime
.strftime(
117 MESSAGE_TIME_FORMAT
) ) )
119 def print_clash_same_time( self
, losePi
, keepPi
):
120 print ( ("Not recording '%s' at %s - it clashes with '%s',"
121 + " which has the same priority (%d). They start at"
122 + " the same time, so the one to record was chosen"
125 losePi
.startTime
.strftime(
126 MESSAGE_TIME_FORMAT
),
127 keepPi
.title
, keepPi
.get_priority() ) )
130 def remove_clashes( self
):
131 self
.record_queue
.sort( priority_time_compare
)
134 for pi1Num
in range( len( self
.record_queue
) ):
135 pi1
= self
.record_queue
[pi1Num
]
139 for pi2
in new_queue
:
141 if pi1
.clashes_with( pi2
):
146 new_queue
.append( pi1
)
148 if pi1
.get_priority() < clashed_with
.get_priority():
149 self
.print_clash_priority_error( pi1
, clashed_with
)
150 elif pi1
.startTime
> clashed_with
.startTime
:
151 self
.print_clash_time_error( pi1
, clashed_with
)
153 self
.print_clash_same_time( pi1
, clashed_with
)
155 self
.record_queue
= new_queue
157 def schedule_recordings( self
, queue
):
159 if len( queue
) == 0:
160 print "No programmes found to record."
163 rtv_utils
.ensure_dir_exists( self
.config
.recorded_progs_dir
)
164 rtv_utils
.ensure_dir_exists( self
.config
.scheduled_events_dir
)
165 rtv_utils
.ensure_dir_exists( self
.config
.recording_log_dir
)
167 for programmeInfo
in queue
:
169 print "Recording '%s' at '%s'" % ( programmeInfo
.title
,
170 programmeInfo
.startTime
.strftime( MESSAGE_TIME_FORMAT
) )
172 filename
= rtv_utils
.prepare_filename( programmeInfo
.title
)
173 filename
+= programmeInfo
.startTime
.strftime( "-%Y-%m-%d_%H_%M" )
175 length_timedelta
= programmeInfo
.endTime
- programmeInfo
.startTime
176 length_in_seconds
= ( ( length_timedelta
.days
177 * rtv_utils
.SECS_IN_DAY
) + length_timedelta
.seconds
)
178 length_in_seconds
+= 60 * self
.config
.extra_recording_time_mins
180 sched_filename
= os
.path
.join( self
.config
.scheduled_events_dir
,
181 filename
+ ".rtvinfo" )
183 outfilename
= os
.path
.join( self
.config
.recorded_progs_dir
,
186 programmeInfo
.startTime
.strftime( "%H:%M %d.%m.%Y" ) )
187 at_output
= rtv_utils
.run_command_feed_input( cmds_array
,
188 self
.config
.record_start_command
% (
189 self
.channel_xmltv2tzap
.get_value( programmeInfo
.channel
),
190 outfilename
, length_in_seconds
, sched_filename
,
191 os
.path
.join( self
.config
.recording_log_dir
,
192 filename
+ ".log" ) ) )
193 at_job_start
= self
.get_at_job( at_output
)
195 if at_job_start
!= None:
196 programmeInfo
.atJob
= at_job_start
197 programmeInfo
.save( sched_filename
)
199 def sax_callback( self
, pi
):
200 for fav
in self
.favs_and_sels
:
201 if fav
.matches( pi
):
202 dtnow
= datetime
.datetime
.today()
203 if( pi
.startTime
> dtnow
204 and (pi
.startTime
- dtnow
).days
< 1 ):
206 if fav
.deleteAfterDays
:
207 pi
.deleteTime
= pi
.endTime
+ datetime
.timedelta(
208 float( fav
.deleteAfterDays
), 0 )
210 pi
.channel_pretty
= self
.channel_xmltv2tzap
.get_value(
212 if not pi
.channel_pretty
:
214 "Pretty channel name not found for channel %s"
217 pi
.priority
= fav
.priority
218 pi
.destination
= fav
.destination
219 pi
.unique_subtitles
= fav
.unique_subtitles
222 pi
.title
= fav
.real_title
224 self
.record_queue
.append( pi
)
227 def remove_scheduled_events( self
):
229 rtv_utils
.ensure_dir_exists( self
.config
.scheduled_events_dir
)
231 for fn
in os
.listdir( self
.config
.scheduled_events_dir
):
232 full_fn
= os
.path
.join( self
.config
.scheduled_events_dir
, fn
)
234 pi
= rtv_programmeinfo
.ProgrammeInfo()
239 at_job_start
= int( pi
.atJob
)
240 rtv_utils
.run_command( ( "atrm", str( at_job_start
) ) )
248 def delete_pending_recordings( self
):
249 rtv_utils
.ensure_dir_exists( self
.config
.recorded_progs_dir
)
251 dttoday
= datetime
.datetime
.today()
253 for fn
in os
.listdir( self
.config
.recorded_progs_dir
):
254 if fn
[-4:] == ".flv":
256 infofn
= os
.path
.join( self
.config
.recorded_progs_dir
,
257 fn
[:-4] + ".rtvinfo" )
259 if os
.path
.isfile( infofn
):
261 pi
= rtv_programmeinfo
.ProgrammeInfo()
264 if pi
.deleteTime
and pi
.deleteTime
< dttoday
:
265 full_fn
= os
.path
.join(
266 self
.config
.recorded_progs_dir
, fn
)
268 print "Deleting file '%s'" % full_fn
275 def schedule( self
, xmltv_parser
= rtv_utils
, fav_reader
= rtv_selection
,
278 if scheduler
== None:
281 # TODO: if we are generating a TV guide as well as scheduling,
282 # we should share the same XML parser.
284 self
.delete_pending_recordings()
286 self
.favs_and_sels
= fav_reader
.read_favs_and_selections(
287 self
.config
, record_only
= True )
289 self
.channel_xmltv2tzap
= rtv_propertiesfile
.PropertiesFile()
290 self
.channel_xmltv2tzap
.load( self
.config
.channel_xmltv2tzap_file
)
292 scheduler
.remove_scheduled_events()
294 self
.record_queue
= []
295 xmltv_parser
.parse_xmltv_files( self
.config
, self
.sax_callback
)
296 self
.remove_already_recorded( scheduler
)
297 self
.remove_clashes()
298 scheduler
.schedule_recordings( self
.record_queue
)
299 self
.record_queue
= []
302 def schedule( config
):
303 sch
= Schedule( config
)
312 def format_title_subtitle( pi
):
319 def format_pi_list( qu
):
322 ret
+= format_title_subtitle( pi
)
325 ret
+= format_title_subtitle( qu
[-1] )
332 def __init__( self
):
334 self
.remove_scheduled_events_called
= False
337 dtnow
= datetime
.datetime
.today()
339 favHeroes
= rtv_favourite
.Favourite()
340 favHeroes
.title_re
= "Heroes"
342 favNewsnight
= rtv_favourite
.Favourite()
343 favNewsnight
.title_re
= "Newsnight.*"
344 favNewsnight
.priority
= -50
346 favLost
= rtv_favourite
.Favourite()
347 favLost
.title_re
= "Lost"
348 favLost
.priority
= -50
350 favPocoyo
= rtv_favourite
.Favourite()
351 favPocoyo
.title_re
= "Pocoyo"
353 self
.test_favs
= [ favHeroes
, favNewsnight
, favLost
, favPocoyo
]
355 self
.piHeroes
= rtv_programmeinfo
.ProgrammeInfo()
356 self
.piHeroes
.title
= "Heroes"
357 self
.piHeroes
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1 )
358 self
.piHeroes
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2 )
359 self
.piHeroes
.channel
= "south-east.bbc2.bbc.co.uk"
361 self
.piNewsnight
= rtv_programmeinfo
.ProgrammeInfo()
362 self
.piNewsnight
.title
= "Newsnight"
363 self
.piNewsnight
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1,
365 self
.piNewsnight
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2 )
366 self
.piNewsnight
.channel
= "south-east.bbc1.bbc.co.uk"
368 self
.piNR
= rtv_programmeinfo
.ProgrammeInfo()
369 self
.piNR
.title
= "Newsnight Review"
370 self
.piNR
.startTime
= dtnow
+ datetime
.timedelta( hours
= 2 )
371 self
.piNR
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2,
373 self
.piNR
.channel
= "south-east.bbc2.bbc.co.uk"
375 self
.piLost
= rtv_programmeinfo
.ProgrammeInfo()
376 self
.piLost
.title
= "Lost"
377 self
.piLost
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1,
379 self
.piLost
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2,
381 self
.piLost
.channel
= "channel4.com"
383 self
.piTurnip
= rtv_programmeinfo
.ProgrammeInfo()
384 self
.piTurnip
.title
= "Newsnight Turnip"
385 self
.piTurnip
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1,
387 self
.piTurnip
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2,
389 self
.piTurnip
.channel
= "channel5.co.uk"
391 self
.piPocoyo1
= rtv_programmeinfo
.ProgrammeInfo()
392 self
.piPocoyo1
.title
= "Pocoyo"
393 self
.piPocoyo1
.sub_title
= "Subtitle already seen"
394 self
.piPocoyo1
.startTime
= dtnow
+ datetime
.timedelta( hours
= 1 )
395 self
.piPocoyo1
.endTime
= dtnow
+ datetime
.timedelta( hours
= 2 )
396 self
.piPocoyo1
.channel
= "south-east.bbc2.bbc.co.uk"
398 self
.piPocoyo2
= rtv_programmeinfo
.ProgrammeInfo()
399 self
.piPocoyo2
.title
= "Pocoyo"
400 self
.piPocoyo2
.sub_title
= "Subtitle not seen"
401 self
.piPocoyo2
.startTime
= dtnow
+ datetime
.timedelta( hours
= 2 )
402 self
.piPocoyo2
.endTime
= dtnow
+ datetime
.timedelta( hours
= 3 )
403 self
.piPocoyo2
.channel
= "south-east.bbc2.bbc.co.uk"
405 self
.which_test
= None
407 def find_old_programmes_map( self
, old_dir
, converted_dir
):
408 return { self
.piPocoyo1
.title
: { self
.piPocoyo1
.sub_title
: 1 } }
410 def parse_xmltv_files( self
, config
, callback
):
411 if self
.which_test
== "no_clash1":
412 callback( self
.piNewsnight
)
413 callback( self
.piNR
)
414 elif self
.which_test
== "priority_clash1":
415 callback( self
.piHeroes
)
416 callback( self
.piNewsnight
)
417 elif self
.which_test
== "time_clash1":
418 callback( self
.piNewsnight
)
419 callback( self
.piLost
)
420 elif self
.which_test
== "same_clash1":
421 callback( self
.piNewsnight
)
422 callback( self
.piTurnip
)
423 elif self
.which_test
== "norerecord":
424 callback( self
.piPocoyo1
)
425 callback( self
.piPocoyo2
)
427 def remove_scheduled_events( self
):
428 if self
.remove_scheduled_events_called
:
429 raise Exception( "remove_scheduled_events called twice." )
430 self
.remove_scheduled_events_called
= True
432 def read_favs_and_selections( self
, config
, record_only
):
433 return self
.test_favs
435 def schedule_recordings( self
, queue
):
439 self
.which_test
= "priority_clash1"
440 self
.schedule
.schedule( self
, self
, self
)
441 if not self
.remove_scheduled_events_called
:
442 raise Exception( "remove_scheduled_events never called" )
443 if self
.queue
!= [self
.piHeroes
]:
444 raise Exception( "queue should look like %s, but it looks like %s"
445 % ( [self
.piHeroes
], self
.queue
) )
448 self
.which_test
= "no_clash1"
449 self
.remove_scheduled_events_called
= False
450 self
.schedule
.schedule( self
, self
, self
)
451 if not self
.remove_scheduled_events_called
:
452 raise Exception( "remove_scheduled_events never called" )
453 if self
.queue
!= [self
.piNewsnight
, self
.piNR
]:
454 raise Exception( "queue should look like %s, but it looks like %s"
455 % ( [self
.piNewsnight
, self
.piNR
], self
.queue
) )
457 self
.which_test
= "time_clash1"
458 self
.remove_scheduled_events_called
= False
459 self
.schedule
.schedule( self
, self
, self
)
460 if not self
.remove_scheduled_events_called
:
461 raise Exception( "remove_scheduled_events never called" )
462 if self
.queue
!= [self
.piLost
]:
463 raise Exception( "queue should look like %s, but it looks like %s"
464 % ( [self
.piLost
], self
.queue
) )
466 self
.which_test
= "same_clash1"
467 self
.remove_scheduled_events_called
= False
468 self
.schedule
.schedule( self
, self
, self
)
469 if not self
.remove_scheduled_events_called
:
470 raise Exception( "remove_scheduled_events never called" )
471 if self
.queue
!= [self
.piNewsnight
] and self
.queue
!= [self
.piTurnip
]:
472 raise Exception( ("queue should look like %s or %s, but it"
474 % ( format_pi_list( [self
.piNewsnight
] ),
475 format_pi_list( [self
.piTurnip
] ),
476 format_pi_list( self
.queue
) ) )
478 def test_norerecord( self
):
479 self
.which_test
= "norerecord"
480 self
.remove_scheduled_events_called
= False
481 self
.schedule
.schedule( self
, self
, self
)
483 if not self
.remove_scheduled_events_called
:
484 raise Exception( "remove_scheduled_events never called" )
485 if self
.queue
!= [self
.piPocoyo2
]:
486 raise Exception( ("queue should look like %s, but it"
488 % ( format_pi_list( [self
.piPocoyo2
] ),
489 format_pi_list( self
.queue
) ) )
493 p
.schedule
= Schedule( config
)