1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import errno
23 import os
24 import time
25 from datetime import datetime
26
27 import gobject
28 import gst
29 import time
30
31 from twisted.internet import reactor
32
33 from flumotion.component import feedcomponent
34 from flumotion.common import log, gstreamer, pygobject, messages, errors
35
36
37 from flumotion.component.component import moods
38 from flumotion.common.pygobject import gsignal
39
40 from flumotion.common.messages import N_
41 T_ = messages.gettexter('flumotion')
42
43 __all__ = ['Disker']
44
45 try:
46
47 from icalendar import Calendar
48 from dateutil import rrule
49 HAS_ICAL = True
50 except:
51 HAS_ICAL = False
52
70
71 -class Disker(feedcomponent.ParseLaunchComponent, log.Loggable):
72 componentMediumClass = DiskerMedium
73 checkOffset = True
74 pipe_template = 'multifdsink sync-method=1 name=fdsink mode=1 sync=false'
75 file = None
76 directory = None
77 location = None
78 caps = None
79
81 self.uiState.addKey('filename', None)
82 self.uiState.addKey('recording', False)
83 self.uiState.addKey('can-schedule', HAS_ICAL)
84
86 directory = properties['directory']
87
88 self.directory = directory
89
90 self.fixRenamedProperties(properties, [('rotateType', 'rotate-type')])
91
92 rotateType = properties.get('rotate-type', 'none')
93
94
95 if not rotateType in ['none', 'size', 'time']:
96 m = messages.Error(T_(N_(
97 "The configuration property 'rotate-type' should be set to "
98 "'size', time', or 'none', not '%s'. "
99 "Please fix the configuration."),
100 rotateType), id='rotate-type')
101 self.addMessage(m)
102 raise errors.ComponentSetupHandledError()
103
104
105 if rotateType in ['size', 'time']:
106 if rotateType not in properties.keys():
107 m = messages.Error(T_(N_(
108 "The configuration property '%s' should be set. "
109 "Please fix the configuration."),
110 rotateType), id='rotate-type')
111 self.addMessage(m)
112 raise errors.ComponentSetupHandledError()
113
114
115 if rotateType == 'size':
116 self.setSizeRotate(properties['size'])
117 elif rotateType == 'time':
118 self.setTimeRotate(properties['time'])
119
120
121 return self.pipe_template
122
128
134
140
150
152 if self.caps:
153 return self.caps.get_structure(0).get_name()
154
156 mime = self.get_mime()
157 if mime == 'multipart/x-mixed-replace':
158 mime += ";boundary=ThisRandomString"
159 return mime
160
162 """
163 @param filenameTemplate: strftime formatted string to decide filename
164 """
165 self.debug("change_filename()")
166 mime = self.get_mime()
167 if mime == 'application/ogg':
168 ext = 'ogg'
169 elif mime == 'multipart/x-mixed-replace':
170 ext = 'multipart'
171 elif mime == 'audio/mpeg':
172 ext = 'mp3'
173 elif mime == 'video/x-msvideo':
174 ext = 'avi'
175 elif mime == 'video/x-ms-asf':
176 ext = 'asf'
177 elif mime == 'audio/x-flac':
178 ext = 'flac'
179 elif mime == 'audio/x-wav':
180 ext = 'wav'
181 elif mime == 'video/x-matroska':
182 ext = 'mkv'
183 elif mime == 'video/x-dv':
184 ext = 'dv'
185 else:
186 ext = 'data'
187
188 sink = self.get_element('fdsink')
189 if sink.get_state() == gst.STATE_NULL:
190 sink.set_state(gst.STATE_READY)
191
192 if self.file:
193 self.file.flush()
194 sink.emit('remove', self.file.fileno())
195 self.file = None
196 if self.symlink_to_last_recording:
197 self.update_symlink(self.location,
198 self.symlink_to_last_recording)
199
200 filename = ""
201 if not filenameTemplate:
202 filenameTemplate = self._defaultFilenameTemplate
203 filename = "%s.%s" % (time.strftime(filenameTemplate,
204 time.localtime()), ext)
205 self.location = os.path.join(self.directory, filename)
206
207 try:
208 self.file = open(self.location, 'a')
209 except IOError, e:
210 self.warning("Failed to open output file %s: %s",
211 self.location, log.getExceptionMessage(e))
212 m = messages.Error(T_(N_("Failed to open output file "
213 "%s. Check your permissions."
214 % (self.location,))))
215 self.addMessage(m)
216 return
217 sink.emit('add', self.file.fileno())
218 self.uiState.set('filename', self.location)
219 self.uiState.set('recording', True)
220
221 if self.symlink_to_current_recording:
222 self.update_symlink(self.location,
223 self.symlink_to_current_recording)
224
226 if not dest.startswith('/'):
227 dest = os.path.join(self.directory, dest)
228 self.debug("updating symbolic link %s to point to %s", src, dest)
229 try:
230 try:
231 os.symlink(src, dest)
232 except OSError, e:
233 if e.errno == errno.EEXIST and os.path.islink(dest):
234 os.unlink(dest)
235 os.symlink(src, dest)
236 else:
237 raise
238 except Exception, e:
239 self.info("Failed to update link %s: %s", dest,
240 log.getExceptionMessage(e))
241 m = messages.Warning(T_(N_("Failed to update symbolic link "
242 "%s. Check your permissions."
243 % (dest,))),
244 debug=log.getExceptionMessage(e))
245 self.state.append('messages', m)
246
248 sink = self.get_element('fdsink')
249 if sink.get_state() == gst.STATE_NULL:
250 sink.set_state(gst.STATE_READY)
251
252 if self.file:
253 self.file.flush()
254 sink.emit('remove', self.file.fileno())
255 self.file = None
256 self.uiState.set('filename', None)
257 self.uiState.set('recording', False)
258 if self.symlink_to_last_recording:
259 self.update_symlink(self.location,
260 self.symlink_to_last_recording)
261
263 caps = pad.get_negotiated_caps()
264 if caps == None:
265 return
266
267 caps_str = gstreamer.caps_repr(caps)
268 self.debug('Got caps: %s' % caps_str)
269
270 new = True
271 if not self.caps == None:
272 self.warning('Already had caps: %s, replacing' % caps_str)
273 new = False
274
275 self.debug('Storing caps: %s' % caps_str)
276 self.caps = caps
277
278 if new and self._recordAtStart:
279 reactor.callLater(0, self.change_filename)
280
281
282
287
299
319
320
321
324 """
325 Sets a recording to start at a time in the future for a specified
326 duration.
327 @param whenStart time of when to start recording
328 @type whenStart datetime
329 @param whenEnd time of when to end recording
330 @type whenEnd datetime
331 @param recur recurrence rule
332 @type recur icalendar.props.vRecur
333 @param filenameTemplate strftime formatted string to decide filename
334 @type filenameTemplate string
335 """
336 now = datetime.now()
337
338 startRecurRule = None
339 endRecurRule = None
340
341 if recur:
342 self.debug("Have a recurrence rule, parsing")
343
344 startRecurRule = rrule.rrulestr(recur.ical(), dtstart=whenStart)
345 endRecurRule = rrule.rrulestr(recur.ical(), dtstart=whenEnd)
346 if now >= whenStart:
347 self.debug("Initial start before now (%r), finding new starts",
348 whenStart)
349 whenStart = startRecurRule.after(now)
350 whenEnd = endRecurRule.after(now)
351 self.debug("New start is now %r", whenStart)
352
353 if now < whenStart:
354 start = whenStart - now
355 startSecs = start.days * 86400 + start.seconds
356 self.debug("scheduling a recording %d seconds away", startSecs)
357 reactor.callLater(startSecs,
358 self.start_scheduled_recording, startRecurRule, whenStart,
359 filenameTemplate)
360 end = whenEnd - now
361 endSecs = end.days * 86400 + end.seconds
362 reactor.callLater(endSecs,
363 self.stop_scheduled_recording, endRecurRule, whenEnd)
364 else:
365 self.warning("attempt to schedule in the past!")
366
368 self.change_filename(filenameTemplate)
369 if recurRule:
370 now = datetime.now()
371 nextTime = recurRule.after(when)
372 recurInterval = nextTime - now
373 self.debug("recurring start interval: %r", recurInterval)
374 recurIntervalSeconds = recurInterval.days * 86400 + \
375 recurInterval.seconds
376 self.debug("recurring start in %d seconds", recurIntervalSeconds)
377 reactor.callLater(recurIntervalSeconds,
378 self.start_scheduled_recording,
379 recurRule, nextTime, filenameTemplate)
380
382 self.stop_recording()
383 if recurRule:
384 now = datetime.now()
385 nextTime = recurRule.after(when)
386 recurInterval = nextTime - now
387 recurIntervalSeconds = recurInterval.days * 86400 + \
388 recurInterval.seconds
389 self.debug("recurring stop in %d seconds", recurIntervalSeconds)
390 reactor.callLater(recurIntervalSeconds,
391 self.stop_scheduled_recording,
392 recurRule, nextTime)
393
395 if HAS_ICAL:
396 cal = Calendar.from_string(icsStr)
397 for event in cal.walk('vevent'):
398 dtstart = event.decoded('dtstart', '')
399 dtend = event.decoded('dtend', '')
400 summary = event.decoded('summary', None)
401 self.debug("event parsed with start: %r end: %r and summary: %s"
402 , dtstart, dtend, summary)
403 recur = event.get('rrule', None)
404 if dtstart and dtend:
405 self.schedule_recording(dtstart, dtend, recur, summary)
406 else:
407 self.warning("Cannot parse ICAL; neccesary modules not installed")
408
409 pygobject.type_register(Disker)
410