cis: bugs fixed; start / stop / remove torrents commands implemented
[living-lab-site.git] / cis / api / base.py
1 #!/usr/bin/env python
2
3 """
4 Base classes for the external programs API.
5 """
6
7 import os
8 import re
9 import random
10
11 import api_exceptions
12 import cis_util
13
14 class BaseTranscoder:
15     """
16     Abstraction of the API class for the transcoder program. 
17     """
18
19     prog_bin = None
20     input_file = None
21     output_file = None
22     dest_path = ''
23     name = None
24
25     # Recommended formats.
26     containers = {
27         "avi": None,
28         "flv": None,
29         "mp4": None,
30         "ogg": None,
31         "webm": None,
32         "mpegts": None
33     }
34     # File extensions by container. First value is for audio files and the
35     # second one is for (audio-)video files.
36     extensions = {
37         "avi": ["avi", "avi"],
38         "flv": ["flv", "flv"],
39         "mp4": ["mp4", "mp4"],
40         "ogg": ["oga", "ogv"],
41         "webm": ["webm", "webm"],
42         "mpegts": ["mts", "mts"]
43     }
44     a_codecs = {
45         "mp3": None,
46         "vorbis": None
47     }
48     v_codecs = {
49         "h264": None,
50         "theora": None,
51         "vp8": None
52     }
53
54     def __init__(self, input_file, name=None, prog_bin=None):
55         self.input_file = input_file
56         if prog_bin is not None:
57             self.prog_bin = prog_bin
58         
59         if name is None:
60             name = cis_util.get_name(input_file)
61
62         self.name = name
63
64     def transcode(self, container, a_codec=None, v_codec=None,
65             a_bitrate=None, a_samplingrate=None, a_channels=None,
66             v_bitrate=None, v_framerate=None, v_resolution=None, v_dar=None):
67         """
68         Transcodes the input file to an audio-video file.
69
70         @param container: possible values are listed in containers member 
71         as keys
72         @param a_codec possible values are listed in a_codecs member as keys
73         @param v_codec possible values are listed in v_codecs member as keys
74         @param a_bitrate (numeric) audio bit rate
75         @param a_samplingrate (numeric) audio sampling rate in Hz
76         @param a_channels (numeric) number of audio channels
77         @param v_bitrate (numeric) video bit rate
78         @param v_framerate (numeric) number of frames per second for a video
79         @param v_resolution (string) video image size as <width>x<height>
80         @param v_dar video display aspect ratio as <den>x<num> or float
81         @return output file name
82         """
83
84         # Check parameters.
85         if a_codec is None and v_codec is None:
86             raise ValueError('No audio or video codec specified.')
87
88         if a_codec is not None and type(a_codec) not in [str, unicode]:
89             raise TypeError('Audio codec must be string.')
90
91         if v_codec is not None and type(v_codec) not in [str, unicode]:
92             raise TypeError('Video codec must be string.')
93
94         if a_samplingrate is not None and type(a_samplingrate) is not int:
95             raise TypeError('Audio sampling rate must be an integer.')
96
97         if a_channels is not None and type(a_channels) is not int:
98             raise TypeError('Audio channels parameter must be an integer.')
99
100         if v_framerate is not None and type(v_framerate) is not int:
101             raise TypeError('Video frate rate must be an integer.')
102
103         if v_resolution is not None \
104                 and re.match('[\d]+x[\d]+', v_resolution) is None:
105             raise ValueError('Video resolution must be a string like <width>x<height>.')
106
107         if v_dar is not None and (type(v_dar) is not float \
108                 and re.match('[\d]+:[\d]+', v_dar) is None):
109             raise ValueError('Video display aspect ratio must be a float or a string like <den>:<num>.')
110
111         self.output_file = os.path.join(self.dest_path, self.name)
112         if v_resolution is not None:
113             self.output_file += '_'
114             self.output_file += v_resolution[(v_resolution.rindex('x')+1):]
115             self.output_file += 'p'
116         ext = self.tr_extension(container, (v_codec is not None))
117         if ext is not None:
118             self.output_file += '.' + ext
119
120         return self._transcode(self.tr_container(container),
121                 self.tr_a_codec(a_codec), self.tr_v_codec(v_codec),
122                 a_bitrate, a_samplingrate, a_channels,
123                 v_bitrate, v_framerate, v_resolution, v_dar)
124
125     def _transcode(self, container, a_codec=None, v_codec=None,
126             a_bitrate=None, a_samplingrate=None, a_channels=None,
127             v_bitrate=None, v_framerate=None, v_resolution=None, v_dar=None):
128         """
129         Called by transcode; must be overridden by a child class which
130         effectively transcodes the input file.
131
132         @return output file name
133         """
134         pass
135
136     def tr_container(self, name):
137         """ Translates container API name into external program identifier."""
138
139         if not self.containers.has_key(name) or self.containers[name] is None:
140             raise api_exceptions.NotImplementedException("Container " + name \
141                     + "not implemented")
142
143         return self.containers[name]
144
145     def tr_extension(self, name, video=True):
146         """ Translates container API name into file extension."""
147
148         if video is True:
149             i = 1
150         else:
151             i = 0
152
153         if not self.extensions.has_key(name) or self.extensions[name] is None:
154             return None
155
156         return self.extensions[name][i]
157
158     def tr_a_codec(self, name):
159         """ Translates audio codec API name into external program identifier."""
160
161         if not self.a_codecs.has_key(name) or self.a_codecs[name] is None:
162             raise api_exceptions.NotImplementedException("Audio Codec " + name \
163                     + "not implemented")
164
165         return self.a_codecs[name]
166
167     def tr_v_codec(self, name):
168         """ Translates video codec API name into external program identifier."""
169
170         if not self.v_codecs.has_key(name) or self.v_codecs[name] is None:
171             raise api_exceptions.NotImplementedException("Video Codec " + name \
172                     + "not implemented")
173
174         return self.v_codecs[name]
175
176
177 class BaseThumbExtractor:
178     """
179     Abstraction of the API class for the thumbnail extraction program. 
180
181     Thumbnail extracted are in JPEG format.
182     """
183
184     prog_bin = None
185     input_file = None
186     dest_path = ''
187     name = None
188     
189     def __init__(self, input_file, name=None, prog_bin=None):
190         self.input_file = input_file
191         if prog_bin is not None:
192             self.prog_bin = prog_bin
193         
194         if name is None:
195             name = cis_util.get_name(input_file)
196
197         self.name = name
198
199     def extract_thumb(self, seek_pos, resolution="120x90", index=0):
200         """
201         Extracts a thumbnail from the video from a specified position
202         expressed in seconds (int/float).
203
204         index: an index appended to the image name in order to avoid
205         overwriting.
206         """
207         pass
208
209     def extract_random_thumb(self, resolution="120x90", index=0):
210         """
211         Extracts a thumbnail from the video from a random position.
212         """
213         duration = self.get_video_duration()
214         seek_pos = random.random() * duration
215         self.extract_thumb(seek_pos, resolution, index)
216
217     def extract_summary_thumbs(self, count, resolution="120x90"):
218         """
219         Extracts a series of thumbnails from a video by taking several
220         snapshots.
221
222         The snapshots are taken from equally spaced positions such that
223         `count` thumbs are extracted.
224         """
225         duration = self.get_video_duration()
226         interval = duration / (count + 1)
227         
228         n_thumbs_extracted = 0
229         seek_pos = interval
230         for index in range (0, count):
231             thumb_extracted = True
232             try:
233                 self.extract_thumb(seek_pos, resolution, n_thumbs_extracted)
234             except api_exceptions.ThumbExtractionException as e:
235                 thumb_extracted = False
236
237             if thumb_extracted:
238                 n_thumbs_extracted += 1
239
240             seek_pos += interval
241
242         return n_thumbs_extracted
243
244     def get_output_file_name(self, index):
245         """ Returns the name required as output file name based on index. """
246         output_file_name = os.path.join(self.dest_path, self.name) \
247                 + '_t' + ("%02d" % index) + '.jpg'
248         return output_file_name
249
250     def get_video_duration(self):
251         pass
252
253
254 class BaseFileTransferer:
255     """
256     Ensures file transfer from the Web Server to the CIS (here).
257     
258     Several implementations can be done by extending this class for
259     file transfer protocol such as FTP, SCP, RSYNC, HTTP etc.
260     """
261     
262     local_path = ''
263     remote_path = ''
264     
265     def __init__(self, local_path='', remote_path=''):
266         """ Initialize by setting local and remote paths for file transfer. """
267         self.local_path = local_path
268         self.remote_path = remote_path
269
270     def __del__(self):
271         self.close()
272
273     def get(self, files):
274         """
275         Transfers files locally from the Web Server.
276
277         files: a list of file name strings
278         """
279         pass
280
281     def put(self, files):
282         """
283         Transfers files from the Web Server locally.
284
285         files: a list of file name strings
286         """
287         pass
288
289     def close(self):
290         """
291         This method should be called when the instance is no longer required.
292
293         Class's destructor calls this method.
294         """
295         pass
296
297
298 class BaseAVInfo:
299     @staticmethod
300     def get_video_duration(input_file, formated=False):
301         """
302         Returns the number of seconds of a video (int/float) if formated is
303         False and a string for duration formated as [HH:]:mm:ss otherwise.
304         """
305         pass