Merge lp:~jderose/dmedia/filestore-ui into lp:dmedia
- filestore-ui
- Merge into trunk
Proposed by
Jason Gerard DeRose
Status: | Merged |
---|---|
Merged at revision: | 345 |
Proposed branch: | lp:~jderose/dmedia/filestore-ui |
Merge into: | lp:dmedia |
Diff against target: |
5276 lines (+517/-4500) 32 files modified
dmedia-service (+29/-10) dmedia/backends/__init__.py (+0/-34) dmedia/backends/s3.py (+0/-184) dmedia/backends/tests/__init__.py (+0/-24) dmedia/backends/tests/test_s3.py (+0/-103) dmedia/backends/tests/test_torrent.py (+0/-69) dmedia/backends/torrent.py (+0/-70) dmedia/core.py (+5/-6) dmedia/gst/__init__.py (+0/-25) dmedia/gst/tests/__init__.py (+0/-24) dmedia/gst/tests/test_transcoder.py (+0/-294) dmedia/gst/transcoder.py (+0/-238) dmedia/gtk/client.py (+0/-238) dmedia/gtk/firstrun.py (+0/-279) dmedia/gtk/menu.py (+0/-408) dmedia/gtk/service.py (+0/-302) dmedia/gtk/tests/test_client.py (+0/-284) dmedia/gtk/tests/test_firstrun.py (+0/-35) dmedia/gtk/tests/test_widgets.py (+0/-142) dmedia/gtk/widgets.py (+0/-225) dmedia/service/tests/test_api.py (+0/-110) dmedia/service/tests/test_udisks.py (+183/-0) dmedia/service/udisks.py (+299/-0) dmedia/webui/__init__.py (+0/-27) dmedia/webui/js.py (+0/-552) dmedia/webui/tests/__init__.py (+0/-24) dmedia/webui/tests/test_basejs.py (+0/-69) dmedia/webui/tests/test_browserjs.py (+0/-38) dmedia/webui/tests/test_couchjs.py (+0/-97) dmedia/webui/tests/test_js.py (+0/-491) dmedia/webui/tests/test_uploaderjs.py (+0/-90) setup.py (+1/-8) |
To merge this branch: | bzr merge lp:~jderose/dmedia/filestore-ui |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
David Jordan | Approve | ||
Review via email: mp+100967@code.launchpad.net |
Commit message
Description of the change
This mostly deletes a bunch of stale Python2 files that weren't being used anymore.
It also fixes setup.py so that doctests and unittests are run for every module (this was changed temporarily quite a while ago during the big refactor).
Last, this real change in this merge is to revamp how dmedia-service uses UDisks. We now do an initial scan of all mounted partitions to find out about any existing FileStore... this fixes the problem of dmedia only properly detected removable drives if you inserted the drive *after* dmedia started.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'dmedia-service' |
2 | --- dmedia-service 2012-03-25 22:39:44 +0000 |
3 | +++ dmedia-service 2012-04-05 13:22:39 +0000 |
4 | @@ -41,7 +41,7 @@ |
5 | |
6 | import dmedia |
7 | from dmedia.core import Core, start_file_server |
8 | -from dmedia.service.dbus import UDisks |
9 | +from dmedia.service.udisks import UDisks |
10 | from dmedia.service.avahi import Avahi |
11 | |
12 | |
13 | @@ -75,8 +75,6 @@ |
14 | if self.env_s is None: |
15 | self.usercouch = usercouch.UserCouch(dmedia.get_dmedia_dir()) |
16 | env = self.usercouch.bootstrap() |
17 | - interval = 5 * 60 * 1000 # Autocompact every 5 minutes |
18 | - self._timeout_id = GObject.timeout_add(interval, self._autocompact) |
19 | else: |
20 | env = json.loads(self.env_s) |
21 | self.udisks = UDisks() |
22 | @@ -86,8 +84,9 @@ |
23 | self.core.add_filestore('/home') |
24 | self.udisks.connect('store_added', self.on_store_added) |
25 | self.udisks.connect('store_removed', self.on_store_removed) |
26 | - self.udisks.monitor() |
27 | - |
28 | + self.udisks.connect('card_added', self.on_card_added) |
29 | + self.udisks.connect('card_removed', self.on_card_removed) |
30 | + |
31 | def start_httpd(self): |
32 | (self.httpd, self.port) = start_file_server(self.core.env) |
33 | self.avahi = Avahi(self.core.env, self.port) |
34 | @@ -95,8 +94,15 @@ |
35 | |
36 | def run(self): |
37 | self.start_core() |
38 | + GObject.timeout_add(500, self.on_idle) |
39 | + self.mainloop.run() |
40 | + |
41 | + def on_idle(self): |
42 | + self.udisks.monitor() |
43 | self.start_httpd() |
44 | - self.mainloop.run() |
45 | + interval = 5 * 60 * 1000 # Autocompact every 5 minutes |
46 | + self._timeout_id = GObject.timeout_add(interval, self._autocompact) |
47 | + return False # Don't repeat idle call |
48 | |
49 | def _autocompact(self): |
50 | log.info('UserCouch.autocompact()...') |
51 | @@ -117,20 +123,32 @@ |
52 | self.httpd.join() |
53 | self.mainloop.quit() |
54 | |
55 | - def on_store_added(self, udisks, obj, parentdir, partition, drive): |
56 | - log.info('UDisks store_added: %r', parentdir) |
57 | + def on_store_added(self, udisks, obj, parentdir, store_id): |
58 | try: |
59 | self.AddFileStore(parentdir) |
60 | except Exception: |
61 | log.exception('Could not add FileStore %r', parentdir) |
62 | |
63 | - def on_store_removed(self, udisks, obj, parentdir): |
64 | - log.info('UDisks store_removed: %r', parentdir) |
65 | + def on_store_removed(self, udisks, obj, parentdir, store_id): |
66 | try: |
67 | self.RemoveFileStore(parentdir) |
68 | except Exception: |
69 | log.exception('Could not remove FileStore %r', parentdir) |
70 | |
71 | + def on_card_added(self, udisks, obj, mount): |
72 | + self.CardAdded(obj, mount) |
73 | + |
74 | + def on_card_removed(self, udisks, obj, mount): |
75 | + self.CardRemoved(obj, mount) |
76 | + |
77 | + @dbus.service.signal(IFACE, signature='ss') |
78 | + def CardAdded(self, obj, mount): |
79 | + pass |
80 | + |
81 | + @dbus.service.signal(IFACE, signature='ss') |
82 | + def CardRemoved(self, obj, mount): |
83 | + pass |
84 | + |
85 | @dbus.service.method(IFACE, in_signature='', out_signature='s') |
86 | def Version(self): |
87 | """ |
88 | @@ -151,6 +169,7 @@ |
89 | """ |
90 | Return dmedia env as JSON string. |
91 | """ |
92 | + log.info('GetEnv') |
93 | return self.env_s |
94 | |
95 | @dbus.service.method(IFACE, in_signature='', out_signature='s') |
96 | |
97 | === removed directory 'dmedia/backends' |
98 | === removed file 'dmedia/backends/__init__.py' |
99 | --- dmedia/backends/__init__.py 2011-04-25 09:03:19 +0000 |
100 | +++ dmedia/backends/__init__.py 1970-01-01 00:00:00 +0000 |
101 | @@ -1,34 +0,0 @@ |
102 | -# Authors: |
103 | -# Jason Gerard DeRose <jderose@novacut.com> |
104 | -# |
105 | -# dmedia: distributed media library |
106 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
107 | -# |
108 | -# This file is part of `dmedia`. |
109 | -# |
110 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
111 | -# terms of the GNU Affero General Public License as published by the Free |
112 | -# Software Foundation, either version 3 of the License, or (at your option) any |
113 | -# later version. |
114 | -# |
115 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
116 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
117 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
118 | -# details. |
119 | -# |
120 | -# You should have received a copy of the GNU Affero General Public License along |
121 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
122 | - |
123 | -""" |
124 | -Plugins for transfer backends for uploading and downloading files. |
125 | -""" |
126 | - |
127 | -try: |
128 | - from . import s3 |
129 | -except ImportError: |
130 | - pass |
131 | - |
132 | -try: |
133 | - from . import torrent |
134 | -except ImportError: |
135 | - pass |
136 | |
137 | === removed file 'dmedia/backends/s3.py' |
138 | --- dmedia/backends/s3.py 2011-09-04 04:04:17 +0000 |
139 | +++ dmedia/backends/s3.py 1970-01-01 00:00:00 +0000 |
140 | @@ -1,184 +0,0 @@ |
141 | -# Authors: |
142 | -# Jason Gerard DeRose <jderose@novacut.com> |
143 | -# |
144 | -# dmedia: distributed media library |
145 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
146 | -# |
147 | -# This file is part of `dmedia`. |
148 | -# |
149 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
150 | -# terms of the GNU Affero General Public License as published by the Free |
151 | -# Software Foundation, either version 3 of the License, or (at your option) any |
152 | -# later version. |
153 | -# |
154 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
155 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
156 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
157 | -# details. |
158 | -# |
159 | -# You should have received a copy of the GNU Affero General Public License along |
160 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
161 | - |
162 | -""" |
163 | -Upload to and download from Amazon S3 using ``boto``. |
164 | - |
165 | -For documentation on ``boto``, see: |
166 | - |
167 | - http://code.google.com/p/boto/ |
168 | -""" |
169 | - |
170 | -import os |
171 | -import logging |
172 | - |
173 | -# FIXME: how do you use gnomekeyring from PyGI? |
174 | -#import gnomekeyring |
175 | -gnomekeyring = None |
176 | -from boto.s3.connection import S3Connection |
177 | -from boto.s3.bucket import Bucket |
178 | -from boto.s3.key import Key |
179 | - |
180 | -from dmedia.constants import LEAF_SIZE |
181 | -from dmedia import transfers |
182 | - |
183 | - |
184 | -log = logging.getLogger() |
185 | - |
186 | - |
187 | -def keyring_name(bucket): |
188 | - """ |
189 | - Keyring name (description) in which to store S3 credentials. |
190 | - |
191 | - For example: |
192 | - |
193 | - >>> keyring_name('novacut') |
194 | - 'dmedia/s3/novacut' |
195 | - |
196 | - """ |
197 | - return 'dmedia/s3/' + bucket |
198 | - |
199 | - |
200 | -def keyring_attrs(bucket): |
201 | - """ |
202 | - Keyring attributes for item storing S3 credentials. |
203 | - |
204 | - For example: |
205 | - |
206 | - >>> keyring_attrs('novacut') |
207 | - {'bucket': 'novacut', 'dmedia': 's3'} |
208 | - |
209 | - """ |
210 | - return { |
211 | - 'dmedia': 's3', |
212 | - 'bucket': bucket, |
213 | - } |
214 | - |
215 | - |
216 | -def save_credentials(bucket, keyid, secret, keyring=gnomekeyring): |
217 | - keyring.item_create_sync( |
218 | - None, |
219 | - gnomekeyring.ITEM_GENERIC_SECRET, |
220 | - keyring_name(bucket), |
221 | - keyring_attrs(bucket), |
222 | - ':'.join([keyid, secret]), |
223 | - True, |
224 | - ) |
225 | - |
226 | - |
227 | -def load_credentials(bucket, keyring=gnomekeyring): |
228 | - raise KeyError # FIXME gnomekeyring |
229 | - try: |
230 | - items = keyring.find_items_sync( |
231 | - gnomekeyring.ITEM_GENERIC_SECRET, |
232 | - keyring_attrs(bucket), |
233 | - ) |
234 | - (keyid, secret) = items[0].secret.split(':') |
235 | - return (keyid, secret) |
236 | - except gnomekeyring.NoMatchError: |
237 | - raise KeyError |
238 | - |
239 | - |
240 | -class S3Backend(transfers.TransferBackend): |
241 | - """ |
242 | - Backend for uploading to and downloading from Amazon S3 using ``boto``. |
243 | - """ |
244 | - def setup(self): |
245 | - self.bucketname = self.store['bucket'] |
246 | - try: |
247 | - (self.keyid, self.secret) = load_credentials(self.bucketname) |
248 | - except KeyError: |
249 | - pass |
250 | - self._bucket = None |
251 | - |
252 | - @property |
253 | - def bucket(self): |
254 | - """ |
255 | - Lazily create the ``boto.s3.bucket.Bucket`` instance. |
256 | - """ |
257 | - if self._bucket is None: |
258 | - conn = S3Connection(self.keyid, self.secret) |
259 | - self._bucket = conn.get_bucket(self.bucketname) |
260 | - return self._bucket |
261 | - |
262 | - def boto_callback(self, completed, total): |
263 | - self.progress(completed) |
264 | - |
265 | - def upload(self, doc, leaves, fs): |
266 | - """ |
267 | - Upload the file with *doc* metadata from the filestore *fs*. |
268 | - |
269 | - :param doc: the CouchDB document of file to upload (a ``dict``) |
270 | - :param fs: a `FileStore` instance from which the file will be read |
271 | - """ |
272 | - chash = doc['_id'] |
273 | - ext = doc.get('ext') |
274 | - key = self.key(chash, ext) |
275 | - log.info('Uploading %r to S3 bucket %r...', key, self.bucketname) |
276 | - |
277 | - k = Key(self.bucket) |
278 | - k.key = key |
279 | - headers = {} |
280 | - if doc.get('content_type'): |
281 | - headers['Content-Type'] = doc['content_type'] |
282 | - if doc.get('name'): |
283 | - headers['Content-Disposition'] = ( |
284 | - 'attachment; filename="{}"'.format(doc['name']) |
285 | - ) |
286 | - |
287 | - if doc.get('content_type') in ('video/ogg', 'audio/ogg'): |
288 | - dur = doc.get('meta', {}).get('duration') |
289 | - if isinstance(dur, int) and dur >= 0: |
290 | - headers['x-amz-meta-content-duration'] = str(dur) |
291 | - fp = fs.open(chash, ext) |
292 | - k.set_contents_from_file(fp, |
293 | - headers=headers, |
294 | - cb=self.boto_callback, |
295 | - num_cb=max(5, doc['bytes'] / LEAF_SIZE), |
296 | - policy='public-read', |
297 | - ) |
298 | - log.info('Uploaded %r to S3 bucket %r', key, self.bucketname) |
299 | - return {'copies': self.copies, 'policy': 'public-read'} |
300 | - |
301 | - def download(self, doc, leaves, fs): |
302 | - """ |
303 | - Download the file with *doc* metadata into the filestore *fs*. |
304 | - |
305 | - :param doc: the CouchDB document of file to download (a ``dict``) |
306 | - :param fs: a `FileStore` instance into which the file will be written |
307 | - """ |
308 | - chash = doc['_id'] |
309 | - ext = doc.get('ext') |
310 | - key = self.key(chash, ext) |
311 | - log.info('Downloading %r from S3 bucket %r...', key, self.bucketname) |
312 | - |
313 | - k = self.bucket.get_key(self.key(chash, ext)) |
314 | - tmp_fp = fs.allocate_for_transfer(doc['bytes'], chash, ext) |
315 | - k.get_file(tmp_fp, |
316 | - cb=self.boto_callback, |
317 | - num_cb=max(5, doc['bytes'] / LEAF_SIZE), |
318 | - ) |
319 | - tmp_fp.close() |
320 | - log.info('Downloaded %r from S3 bucket %r', key, self.bucketname) |
321 | - |
322 | - |
323 | -transfers.register_uploader('s3', S3Backend) |
324 | -transfers.register_downloader('s3', S3Backend) |
325 | |
326 | === removed directory 'dmedia/backends/tests' |
327 | === removed file 'dmedia/backends/tests/__init__.py' |
328 | --- dmedia/backends/tests/__init__.py 2011-04-23 07:29:32 +0000 |
329 | +++ dmedia/backends/tests/__init__.py 1970-01-01 00:00:00 +0000 |
330 | @@ -1,24 +0,0 @@ |
331 | -# Authors: |
332 | -# Jason Gerard DeRose <jderose@novacut.com> |
333 | -# |
334 | -# dmedia: distributed media library |
335 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
336 | -# |
337 | -# This file is part of `dmedia`. |
338 | -# |
339 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
340 | -# terms of the GNU Affero General Public License as published by the Free |
341 | -# Software Foundation, either version 3 of the License, or (at your option) any |
342 | -# later version. |
343 | -# |
344 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
345 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
346 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
347 | -# details. |
348 | -# |
349 | -# You should have received a copy of the GNU Affero General Public License along |
350 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
351 | - |
352 | -""" |
353 | -Unit tests for the `dmedia.backends` package. |
354 | -""" |
355 | |
356 | === removed file 'dmedia/backends/tests/test_s3.py' |
357 | --- dmedia/backends/tests/test_s3.py 2011-09-04 04:04:17 +0000 |
358 | +++ dmedia/backends/tests/test_s3.py 1970-01-01 00:00:00 +0000 |
359 | @@ -1,103 +0,0 @@ |
360 | -# Authors: |
361 | -# Jason Gerard DeRose <jderose@novacut.com> |
362 | -# |
363 | -# dmedia: distributed media library |
364 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
365 | -# |
366 | -# This file is part of `dmedia`. |
367 | -# |
368 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
369 | -# terms of the GNU Affero General Public License as published by the Free |
370 | -# Software Foundation, either version 3 of the License, or (at your option) any |
371 | -# later version. |
372 | -# |
373 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
374 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
375 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
376 | -# details. |
377 | -# |
378 | -# You should have received a copy of the GNU Affero General Public License along |
379 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
380 | - |
381 | -""" |
382 | -Unit tests for the `dmedia.backends.s3` module. |
383 | -""" |
384 | - |
385 | -from unittest import TestCase |
386 | - |
387 | -# FIXME: how do you use gnomekeyring from PyGI? |
388 | -#import gnomekeyring |
389 | - |
390 | -from dmedia.backends import s3 |
391 | - |
392 | - |
393 | -class DummyItem: |
394 | - secret = 'bar:baz' |
395 | - |
396 | - |
397 | -class DummyKeyring(object): |
398 | - def item_create_sync(self, *args): |
399 | - self._create = args |
400 | - |
401 | - def find_items_sync(self, *args): |
402 | - self._find = args |
403 | - return [DummyItem()] |
404 | - |
405 | - |
406 | -class TestFunctions(TestCase): |
407 | - def test_keyring_name(self): |
408 | - f = s3.keyring_name |
409 | - self.assertEqual(f('whatever'), 'dmedia/s3/whatever') |
410 | - |
411 | - def test_keyring_attrs(self): |
412 | - f = s3.keyring_attrs |
413 | - self.assertEqual(f('whatever'), {'bucket': 'whatever', 'dmedia': 's3'}) |
414 | - |
415 | - def test_save_credentials(self): |
416 | - self.skipTest('gnomekeyring') |
417 | - f = s3.save_credentials |
418 | - k = DummyKeyring() |
419 | - self.assertIsNone(f('foo', 'bar', 'baz', k)) |
420 | - self.assertEqual( |
421 | - k._create, |
422 | - ( |
423 | - None, |
424 | - gnomekeyring.ITEM_GENERIC_SECRET, |
425 | - 'dmedia/s3/foo', |
426 | - {'bucket': 'foo', 'dmedia': 's3'}, |
427 | - 'bar:baz', |
428 | - True, |
429 | - ) |
430 | - ) |
431 | - |
432 | - def test_load_credentials(self): |
433 | - self.skipTest('gnomekeyring') |
434 | - f = s3.load_credentials |
435 | - k = DummyKeyring() |
436 | - self.assertEqual(f('foo', k), ('bar', 'baz')) |
437 | - self.assertEqual( |
438 | - k._find, |
439 | - ( |
440 | - gnomekeyring.ITEM_GENERIC_SECRET, |
441 | - {'bucket': 'foo', 'dmedia': 's3'}, |
442 | - ) |
443 | - ) |
444 | - |
445 | - |
446 | - |
447 | -class TestS3Backend(TestCase): |
448 | - klass = s3.S3Backend |
449 | - |
450 | - def test_init(self): |
451 | - inst = self.klass({'_id': 'foo', 'bucket': 'bar'}) |
452 | - self.assertEqual(inst.bucketname, 'bar') |
453 | - self.assertEqual(inst._bucket, None) |
454 | - |
455 | - def test_repr(self): |
456 | - inst = self.klass({'_id': 'foo', 'bucket': 'bar'}) |
457 | - self.assertEqual(repr(inst), "S3Backend('foo')") |
458 | - |
459 | - def test_bucket(self): |
460 | - inst = self.klass({'_id': 'foo', 'bucket': 'bar'}) |
461 | - inst._bucket = 'whatever' |
462 | - self.assertEqual(inst.bucket, 'whatever') |
463 | |
464 | === removed file 'dmedia/backends/tests/test_torrent.py' |
465 | --- dmedia/backends/tests/test_torrent.py 2011-04-25 09:03:19 +0000 |
466 | +++ dmedia/backends/tests/test_torrent.py 1970-01-01 00:00:00 +0000 |
467 | @@ -1,69 +0,0 @@ |
468 | -# Authors: |
469 | -# Jason Gerard DeRose <jderose@novacut.com> |
470 | -# |
471 | -# dmedia: distributed media library |
472 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
473 | -# |
474 | -# This file is part of `dmedia`. |
475 | -# |
476 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
477 | -# terms of the GNU Affero General Public License as published by the Free |
478 | -# Software Foundation, either version 3 of the License, or (at your option) any |
479 | -# later version. |
480 | -# |
481 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
482 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
483 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
484 | -# details. |
485 | -# |
486 | -# You should have received a copy of the GNU Affero General Public License along |
487 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
488 | - |
489 | -""" |
490 | -Unit tests for the `dmedia.backends.torrent` module. |
491 | -""" |
492 | - |
493 | -from unittest import TestCase |
494 | -import httplib |
495 | - |
496 | -from dmedia.backends import torrent |
497 | - |
498 | - |
499 | -class TestTorrentBackend(TestCase): |
500 | - klass = torrent.TorrentBackend |
501 | - |
502 | - def test_init(self): |
503 | - url = 'https://foo.s3.amazonaws.com/' |
504 | - inst = self.klass({'url': url}) |
505 | - self.assertEqual(inst.url, url) |
506 | - self.assertEqual(inst.basepath, '/') |
507 | - self.assertEqual( |
508 | - inst.t, |
509 | - ('https', 'foo.s3.amazonaws.com', '/', '', '', '') |
510 | - ) |
511 | - self.assertIsInstance(inst.conn, httplib.HTTPSConnection) |
512 | - |
513 | - url = 'http://example.com/bar' |
514 | - inst = self.klass({'url': url}) |
515 | - self.assertEqual(inst.url, url) |
516 | - self.assertEqual(inst.basepath, '/bar/') |
517 | - self.assertEqual( |
518 | - inst.t, |
519 | - ('http', 'example.com', '/bar', '', '', '') |
520 | - ) |
521 | - self.assertIsInstance(inst.conn, httplib.HTTPConnection) |
522 | - self.assertNotIsInstance(inst.conn, httplib.HTTPSConnection) |
523 | - |
524 | - with self.assertRaises(ValueError) as cm: |
525 | - inst = self.klass({'url': 'ftp://example.com/'}) |
526 | - self.assertEqual( |
527 | - str(cm.exception), |
528 | - "url scheme must be http or https; got 'ftp://example.com/'" |
529 | - ) |
530 | - |
531 | - with self.assertRaises(ValueError) as cm: |
532 | - inst = self.klass({'url': 'http:example.com/bar'}) |
533 | - self.assertEqual( |
534 | - str(cm.exception), |
535 | - "bad url: 'http:example.com/bar'" |
536 | - ) |
537 | |
538 | === removed file 'dmedia/backends/torrent.py' |
539 | --- dmedia/backends/torrent.py 2011-04-25 09:03:19 +0000 |
540 | +++ dmedia/backends/torrent.py 1970-01-01 00:00:00 +0000 |
541 | @@ -1,70 +0,0 @@ |
542 | -# Authors: |
543 | -# Jason Gerard DeRose <jderose@novacut.com> |
544 | -# |
545 | -# dmedia: distributed media library |
546 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
547 | -# |
548 | -# This file is part of `dmedia`. |
549 | -# |
550 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
551 | -# terms of the GNU Affero General Public License as published by the Free |
552 | -# Software Foundation, either version 3 of the License, or (at your option) any |
553 | -# later version. |
554 | -# |
555 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
556 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
557 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
558 | -# details. |
559 | -# |
560 | -# You should have received a copy of the GNU Affero General Public License along |
561 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
562 | - |
563 | -""" |
564 | -Download with BitTorrent. |
565 | -""" |
566 | - |
567 | -import logging |
568 | -import time |
569 | -from os import path |
570 | - |
571 | -import libtorrent |
572 | - |
573 | -from dmedia.constants import LEAF_SIZE |
574 | -from dmedia.transfers import HTTPBaseBackend, register_downloader, http_conn |
575 | - |
576 | - |
577 | -log = logging.getLogger() |
578 | - |
579 | - |
580 | -class TorrentBackend(HTTPBaseBackend): |
581 | - """ |
582 | - Backend for BitTorrent downloads using `libtorrent`. |
583 | - """ |
584 | - |
585 | - def download(self, doc, leaves, fs): |
586 | - chash = doc['_id'] |
587 | - ext = doc.get('ext') |
588 | - url = self.basepath + self.key(chash, ext) + '?torrent' |
589 | - data = self.get(url) |
590 | - |
591 | - tmp = fs.tmp(chash, ext, create=True) |
592 | - session = libtorrent.session() |
593 | - session.listen_on(6881, 6891) |
594 | - |
595 | - info = libtorrent.torrent_info(libtorrent.bdecode(data)) |
596 | - |
597 | - torrent = session.add_torrent({ |
598 | - 'ti': info, |
599 | - 'save_path': path.dirname(tmp), |
600 | - }) |
601 | - |
602 | - while not torrent.is_seed(): |
603 | - s = torrent.status() |
604 | - self.progress(s.total_payload_download) |
605 | - time.sleep(2) |
606 | - |
607 | - session.remove_torrent(torrent) |
608 | - time.sleep(2) |
609 | - |
610 | - |
611 | -register_downloader('torrent', TorrentBackend) |
612 | |
613 | === modified file 'dmedia/core.py' |
614 | --- dmedia/core.py 2012-01-23 17:56:15 +0000 |
615 | +++ dmedia/core.py 2012-04-05 13:22:39 +0000 |
616 | @@ -98,7 +98,6 @@ |
617 | class Base: |
618 | def __init__(self, env): |
619 | self.env = env |
620 | - self.db = Database(schema.DB_NAME, env) |
621 | self.logdb = Database('dmedia_log', env) |
622 | |
623 | def log(self, doc): |
624 | @@ -107,9 +106,10 @@ |
625 | return self.logdb.post(doc, batch='ok') |
626 | |
627 | |
628 | -class Core(Base): |
629 | +class Core: |
630 | def __init__(self, env, get_parentdir_info=None, bootstrap=True): |
631 | - super().__init__(env) |
632 | + self.env = env |
633 | + self.db = Database(schema.DB_NAME, env) |
634 | self.stores = LocalStores() |
635 | self._get_parentdir_info = get_parentdir_info |
636 | if bootstrap: |
637 | @@ -117,7 +117,6 @@ |
638 | |
639 | def _bootstrap(self): |
640 | self.db.ensure() |
641 | - self.logdb.ensure() |
642 | init_views(self.db) |
643 | self._init_local() |
644 | self._init_stores() |
645 | @@ -169,8 +168,8 @@ |
646 | doc['connected'] = time.time() |
647 | doc['connected_to'] = self.machine_id |
648 | doc['statvfs'] = fs.statvfs()._asdict() |
649 | - if callable(self._get_parentdir_info): |
650 | - doc.update(self._get_parentdir_info(parentdir)) |
651 | + #if callable(self._get_parentdir_info): |
652 | + # doc.update(self._get_parentdir_info(parentdir)) |
653 | self.db.save(doc) |
654 | log.info('FileStore %r at %r', fs.id, fs.parentdir) |
655 | return fs |
656 | |
657 | === removed directory 'dmedia/gst' |
658 | === removed file 'dmedia/gst/__init__.py' |
659 | --- dmedia/gst/__init__.py 2011-09-16 03:22:56 +0000 |
660 | +++ dmedia/gst/__init__.py 1970-01-01 00:00:00 +0000 |
661 | @@ -1,25 +0,0 @@ |
662 | -# Authors: |
663 | -# Jason Gerard DeRose <jderose@novacut.com> |
664 | -# |
665 | -# dmedia: distributed media library |
666 | -# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com> |
667 | -# |
668 | -# This file is part of `dmedia`. |
669 | -# |
670 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
671 | -# terms of the GNU Affero General Public License as published by the Free |
672 | -# Software Foundation, either version 3 of the License, or (at your option) any |
673 | -# later version. |
674 | -# |
675 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
676 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
677 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
678 | -# details. |
679 | -# |
680 | -# You should have received a copy of the GNU Affero General Public License along |
681 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
682 | - |
683 | -""" |
684 | -GStreamer PyGI backend components. |
685 | -""" |
686 | - |
687 | |
688 | === removed directory 'dmedia/gst/tests' |
689 | === removed file 'dmedia/gst/tests/__init__.py' |
690 | --- dmedia/gst/tests/__init__.py 2011-09-16 03:22:56 +0000 |
691 | +++ dmedia/gst/tests/__init__.py 1970-01-01 00:00:00 +0000 |
692 | @@ -1,24 +0,0 @@ |
693 | -# Authors: |
694 | -# Jason Gerard DeRose <jderose@novacut.com> |
695 | -# |
696 | -# dmedia: distributed media library |
697 | -# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com> |
698 | -# |
699 | -# This file is part of `dmedia`. |
700 | -# |
701 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
702 | -# terms of the GNU Affero General Public License as published by the Free |
703 | -# Software Foundation, either version 3 of the License, or (at your option) any |
704 | -# later version. |
705 | -# |
706 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
707 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
708 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
709 | -# details. |
710 | -# |
711 | -# You should have received a copy of the GNU Affero General Public License along |
712 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
713 | - |
714 | -""" |
715 | -Unit tests for the `dmedia.gst` subpackage. |
716 | -""" |
717 | |
718 | === removed file 'dmedia/gst/tests/test_transcoder.py' |
719 | --- dmedia/gst/tests/test_transcoder.py 2011-09-16 03:22:56 +0000 |
720 | +++ dmedia/gst/tests/test_transcoder.py 1970-01-01 00:00:00 +0000 |
721 | @@ -1,294 +0,0 @@ |
722 | -# Authors: |
723 | -# Jason Gerard DeRose <jderose@novacut.com> |
724 | -# |
725 | -# dmedia: distributed media library |
726 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
727 | -# |
728 | -# This file is part of `dmedia`. |
729 | -# |
730 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
731 | -# terms of the GNU Affero General Public License as published by the Free |
732 | -# Software Foundation, either version 3 of the License, or (at your option) any |
733 | -# later version. |
734 | -# |
735 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
736 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
737 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
738 | -# details. |
739 | -# |
740 | -# You should have received a copy of the GNU Affero General Public License along |
741 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
742 | - |
743 | -""" |
744 | -Unit tests for `dmedia.transcoder` module. |
745 | -""" |
746 | - |
747 | -from unittest import TestCase |
748 | - |
749 | -from gi.repository import Gst |
750 | - |
751 | -from dmedia import transcoder |
752 | -from dmedia.constants import TYPE_ERROR |
753 | -from dmedia.filestore import FileStore |
754 | -from .helpers import sample_mov, mov_hash, TempDir, raises |
755 | - |
756 | - |
757 | -class test_functions(TestCase): |
758 | - def test_caps_string(self): |
759 | - f = transcoder.caps_string |
760 | - self.assertEqual( |
761 | - f('audio/x-raw-float', {}), |
762 | - 'audio/x-raw-float' |
763 | - ) |
764 | - self.assertEqual( |
765 | - f('audio/x-raw-float', {'rate': 44100}), |
766 | - 'audio/x-raw-float, rate=44100' |
767 | - ) |
768 | - self.assertEqual( |
769 | - f('audio/x-raw-float', {'rate': 44100, 'channels': 1}), |
770 | - 'audio/x-raw-float, channels=1, rate=44100' |
771 | - ) |
772 | - |
773 | - |
774 | -class test_TranscodBin(TestCase): |
775 | - klass = transcoder.TranscodeBin |
776 | - |
777 | - def test_init(self): |
778 | - d = { |
779 | - 'enc': 'vorbisenc', |
780 | - 'props': { |
781 | - 'quality': 0.5, |
782 | - }, |
783 | - } |
784 | - inst = self.klass(d) |
785 | - self.assertTrue(inst._d is d) |
786 | - |
787 | - self.assertTrue(inst._q1.get_parent() is inst) |
788 | - self.assertTrue(isinstance(inst._q1, Gst.Element)) |
789 | - self.assertEqual(inst._q1.get_factory().get_name(), 'queue') |
790 | - |
791 | - self.assertTrue(inst._enc.get_parent() is inst) |
792 | - self.assertTrue(isinstance(inst._enc, Gst.Element)) |
793 | - self.assertEqual(inst._enc.get_factory().get_name(), 'vorbisenc') |
794 | - self.assertEqual(inst._enc.get_property('quality'), 0.5) |
795 | - |
796 | - self.assertTrue(inst._q2.get_parent() is inst) |
797 | - self.assertTrue(isinstance(inst._q2, Gst.Element)) |
798 | - self.assertEqual(inst._q2.get_factory().get_name(), 'queue') |
799 | - |
800 | - d = {'enc': 'vorbisenc'} |
801 | - inst = self.klass(d) |
802 | - self.assertTrue(inst._d is d) |
803 | - |
804 | - self.assertTrue(inst._q1.get_parent() is inst) |
805 | - self.assertTrue(isinstance(inst._q1, Gst.Element)) |
806 | - self.assertEqual(inst._q1.get_factory().get_name(), 'queue') |
807 | - |
808 | - self.assertTrue(inst._enc.get_parent() is inst) |
809 | - self.assertTrue(isinstance(inst._enc, Gst.Element)) |
810 | - self.assertEqual(inst._enc.get_factory().get_name(), 'vorbisenc') |
811 | - self.assertNotEqual(inst._enc.get_property('quality'), 0.5) |
812 | - |
813 | - self.assertTrue(inst._q2.get_parent() is inst) |
814 | - self.assertTrue(isinstance(inst._q2, Gst.Element)) |
815 | - self.assertEqual(inst._q2.get_factory().get_name(), 'queue') |
816 | - |
817 | - def test_repr(self): |
818 | - d = { |
819 | - 'enc': 'vorbisenc', |
820 | - 'props': { |
821 | - 'quality': 0.5, |
822 | - }, |
823 | - } |
824 | - |
825 | - inst = self.klass(d) |
826 | - self.assertEqual( |
827 | - repr(inst), |
828 | - 'TranscodeBin(%r)' % (d,) |
829 | - ) |
830 | - |
831 | - class FooBar(self.klass): |
832 | - pass |
833 | - inst = FooBar(d) |
834 | - self.assertEqual( |
835 | - repr(inst), |
836 | - 'FooBar(%r)' % (d,) |
837 | - ) |
838 | - |
839 | - def test_make(self): |
840 | - d = {'enc': 'vorbisenc'} |
841 | - inst = self.klass(d) |
842 | - |
843 | - enc = inst._make('theoraenc') |
844 | - self.assertTrue(enc.get_parent() is inst) |
845 | - self.assertTrue(isinstance(enc, Gst.Element)) |
846 | - self.assertEqual(enc.get_factory().get_name(), 'theoraenc') |
847 | - self.assertEqual(enc.get_property('quality'), 48) |
848 | - self.assertEqual(enc.get_property('keyframe-force'), 64) |
849 | - |
850 | - enc = inst._make('theoraenc', {'quality': 50, 'keyframe-force': 32}) |
851 | - self.assertTrue(enc.get_parent() is inst) |
852 | - self.assertTrue(isinstance(enc, Gst.Element)) |
853 | - self.assertEqual(enc.get_factory().get_name(), 'theoraenc') |
854 | - self.assertEqual(enc.get_property('quality'), 50) |
855 | - self.assertEqual(enc.get_property('keyframe-force'), 32) |
856 | - |
857 | - |
858 | -class test_AudioTranscoder(TestCase): |
859 | - klass = transcoder.AudioTranscoder |
860 | - |
861 | - def test_init(self): |
862 | - d = { |
863 | - 'enc': 'vorbisenc', |
864 | - 'props': { |
865 | - 'quality': 0.5, |
866 | - }, |
867 | - } |
868 | - inst = self.klass(d) |
869 | - self.assertTrue(isinstance(inst._enc, Gst.Element)) |
870 | - self.assertEqual(inst._enc.get_factory().get_name(), 'vorbisenc') |
871 | - self.assertEqual(inst._enc.get_property('quality'), 0.5) |
872 | - |
873 | - d = { |
874 | - 'enc': 'vorbisenc', |
875 | - 'caps': {'rate': 44100}, |
876 | - 'props': {'quality': 0.25}, |
877 | - } |
878 | - inst = self.klass(d) |
879 | - self.assertTrue(isinstance(inst._enc, Gst.Element)) |
880 | - self.assertEqual(inst._enc.get_factory().get_name(), 'vorbisenc') |
881 | - self.assertEqual(inst._enc.get_property('quality'), 0.25) |
882 | - |
883 | - |
884 | -class test_VideoTranscoder(TestCase): |
885 | - klass = transcoder.VideoTranscoder |
886 | - |
887 | - def test_init(self): |
888 | - d = { |
889 | - 'enc': 'theoraenc', |
890 | - 'props': { |
891 | - 'quality': 50, |
892 | - 'keyframe-force': 32, |
893 | - }, |
894 | - } |
895 | - inst = self.klass(d) |
896 | - self.assertTrue(isinstance(inst._enc, Gst.Element)) |
897 | - self.assertEqual(inst._enc.get_factory().get_name(), 'theoraenc') |
898 | - self.assertEqual(inst._enc.get_property('quality'), 50) |
899 | - |
900 | - d = { |
901 | - 'enc': 'theoraenc', |
902 | - 'caps': {'width': 800, 'height': 450}, |
903 | - 'props': { |
904 | - 'quality': 50, |
905 | - 'keyframe-force': 32, |
906 | - }, |
907 | - } |
908 | - inst = self.klass(d) |
909 | - self.assertTrue(isinstance(inst._enc, Gst.Element)) |
910 | - self.assertEqual(inst._enc.get_factory().get_name(), 'theoraenc') |
911 | - self.assertEqual(inst._enc.get_property('quality'), 50) |
912 | - |
913 | - |
914 | -class test_Transcoder(TestCase): |
915 | - klass = transcoder.Transcoder |
916 | - |
917 | - def setUp(self): |
918 | - self.tmp = TempDir() |
919 | - self.fs = FileStore(self.tmp.path) |
920 | - self.fs.import_file(open(sample_mov, 'rb'), 'mov') |
921 | - |
922 | - def test_init(self): |
923 | - job = { |
924 | - 'src': {'id': mov_hash, 'ext': 'mov'}, |
925 | - 'mux': 'oggmux', |
926 | - 'ext': 'ogv', |
927 | - } |
928 | - |
929 | - e = raises(TypeError, self.klass, 17, self.fs) |
930 | - self.assertEqual( |
931 | - str(e), |
932 | - TYPE_ERROR % ('job', dict, int, 17) |
933 | - ) |
934 | - e = raises(TypeError, self.klass, job, 18) |
935 | - self.assertEqual( |
936 | - str(e), |
937 | - TYPE_ERROR % ('fs', FileStore, int, 18) |
938 | - ) |
939 | - |
940 | - inst = self.klass(job, self.fs) |
941 | - self.assertTrue(inst.job is job) |
942 | - self.assertTrue(inst.fs is self.fs) |
943 | - |
944 | - self.assertTrue(isinstance(inst.dst_fp, file)) |
945 | - self.assertEqual(inst.dst_fp.mode, 'r+b') |
946 | - self.assertTrue( |
947 | - inst.dst_fp.name.startswith(self.tmp.join('.dmedia', 'writes')) |
948 | - ) |
949 | - self.assertTrue(inst.dst_fp.name.endswith('.ogv')) |
950 | - |
951 | - self.assertTrue(isinstance(inst.src, Gst.Element)) |
952 | - self.assertTrue(inst.src.get_parent() is inst.pipeline) |
953 | - self.assertEqual(inst.src.get_factory().get_name(), 'filesrc') |
954 | - self.assertEqual( |
955 | - inst.src.get_property('location'), |
956 | - self.fs.path(mov_hash, 'mov') |
957 | - ) |
958 | - |
959 | - self.assertTrue(isinstance(inst.dec, Gst.Element)) |
960 | - self.assertTrue(inst.dec.get_parent() is inst.pipeline) |
961 | - self.assertEqual(inst.dec.get_factory().get_name(), 'decodebin2') |
962 | - |
963 | - self.assertTrue(isinstance(inst.mux, Gst.Element)) |
964 | - self.assertTrue(inst.mux.get_parent() is inst.pipeline) |
965 | - self.assertEqual(inst.mux.get_factory().get_name(), 'oggmux') |
966 | - |
967 | - self.assertTrue(isinstance(inst.sink, Gst.Element)) |
968 | - self.assertTrue(inst.sink.get_parent() is inst.pipeline) |
969 | - self.assertEqual(inst.sink.get_factory().get_name(), 'fdsink') |
970 | - self.assertEqual(inst.sink.get_property('fd'), inst.dst_fp.fileno()) |
971 | - |
972 | - def test_theora450(self): |
973 | - self.skipTest('fix me gi.repository.Gst') |
974 | - job = { |
975 | - 'src': {'id': mov_hash, 'ext': 'mov'}, |
976 | - 'mux': 'oggmux', |
977 | - 'video': { |
978 | - 'enc': 'theoraenc', |
979 | - 'caps': {'width': 800, 'height': 450}, |
980 | - }, |
981 | - } |
982 | - inst = self.klass(job, self.fs) |
983 | - inst.run() |
984 | - |
985 | - def test_flac(self): |
986 | - self.skipTest('fix me gi.repository.Gst') |
987 | - job = { |
988 | - 'src': {'id': mov_hash, 'ext': 'mov'}, |
989 | - 'mux': 'oggmux', |
990 | - 'audio': { |
991 | - 'enc': 'flacenc', |
992 | - 'caps': {'rate': 44100}, |
993 | - }, |
994 | - } |
995 | - inst = self.klass(job, self.fs) |
996 | - inst.run() |
997 | - |
998 | - def test_theora360_vorbis(self): |
999 | - self.skipTest('fix me gi.repository.Gst') |
1000 | - job = { |
1001 | - 'src': {'id': mov_hash, 'ext': 'mov'}, |
1002 | - 'mux': 'oggmux', |
1003 | - 'video': { |
1004 | - 'enc': 'theoraenc', |
1005 | - 'props': {'quality': 40}, |
1006 | - 'caps': {'width': 800, 'height': 450}, |
1007 | - }, |
1008 | - 'audio': { |
1009 | - 'enc': 'vorbisenc', |
1010 | - 'props': {'quality': 0.4}, |
1011 | - 'caps': {'rate': 44100}, |
1012 | - }, |
1013 | - } |
1014 | - inst = self.klass(job, self.fs) |
1015 | - inst.run() |
1016 | |
1017 | === removed file 'dmedia/gst/transcoder.py' |
1018 | --- dmedia/gst/transcoder.py 2011-09-16 03:22:56 +0000 |
1019 | +++ dmedia/gst/transcoder.py 1970-01-01 00:00:00 +0000 |
1020 | @@ -1,238 +0,0 @@ |
1021 | -# Authors: |
1022 | -# Jason Gerard DeRose <jderose@novacut.com> |
1023 | -# |
1024 | -# dmedia: distributed media library |
1025 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
1026 | -# |
1027 | -# This file is part of `dmedia`. |
1028 | -# |
1029 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
1030 | -# terms of the GNU Affero General Public License as published by the Free |
1031 | -# Software Foundation, either version 3 of the License, or (at your option) any |
1032 | -# later version. |
1033 | -# |
1034 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
1035 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
1036 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
1037 | -# details. |
1038 | -# |
1039 | -# You should have received a copy of the GNU Affero General Public License along |
1040 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
1041 | - |
1042 | -""" |
1043 | -GStreamer-based transcoder. |
1044 | -""" |
1045 | - |
1046 | -import logging |
1047 | - |
1048 | -from gi.repository import GObject, Gst |
1049 | - |
1050 | -from .constants import TYPE_ERROR |
1051 | -from .filestore import FileStore |
1052 | - |
1053 | - |
1054 | -log = logging.getLogger() |
1055 | -GObject.threads_init() |
1056 | -Gst.init(None) |
1057 | - |
1058 | - |
1059 | -def caps_string(mime, caps): |
1060 | - """ |
1061 | - Build a GStreamer caps string. |
1062 | - |
1063 | - For example: |
1064 | - |
1065 | - >>> caps_string('video/x-raw-yuv', {'width': 800, 'height': 450}) |
1066 | - 'video/x-raw-yuv, height=450, width=800' |
1067 | - """ |
1068 | - accum = [mime] |
1069 | - for key in sorted(caps): |
1070 | - accum.append('%s=%s' % (key, caps[key])) |
1071 | - return ', '.join(accum) |
1072 | - |
1073 | - |
1074 | - |
1075 | -class TranscodeBin(Gst.Bin): |
1076 | - """ |
1077 | - Base class for `AudioTranscoder` and `VideoTranscoder`. |
1078 | - """ |
1079 | - def __init__(self, d): |
1080 | - super(TranscodeBin, self).__init__() |
1081 | - self._d = d |
1082 | - self._q1 = self._make('queue') |
1083 | - self._enc = self._make(d['enc'], d.get('props')) |
1084 | - self._q2 = self._make('queue') |
1085 | - self._enc.link(self._q2) |
1086 | - self.add_pad( |
1087 | - Gst.GhostPad.new('sink', self._q1.get_pad('sink')) |
1088 | - ) |
1089 | - self.add_pad( |
1090 | - Gst.GhostPad.new('src', self._q2.get_pad('src')) |
1091 | - ) |
1092 | - |
1093 | - def __repr__(self): |
1094 | - return '%s(%r)' % (self.__class__.__name__, self._d) |
1095 | - |
1096 | - def _make(self, name, props=None): |
1097 | - """ |
1098 | - Create gst element, set properties, and add to this bin. |
1099 | - """ |
1100 | - element = Gst.ElementFactory.make(name, None) |
1101 | - if props: |
1102 | - for (key, value) in props.iteritems(): |
1103 | - element.set_property(key, value) |
1104 | - self.add(element) |
1105 | - return element |
1106 | - |
1107 | - |
1108 | -class AudioTranscoder(TranscodeBin): |
1109 | - def __init__(self, d): |
1110 | - super(AudioTranscoder, self).__init__(d) |
1111 | - |
1112 | - # Create processing elements: |
1113 | - self._conv = self._make('audioconvert') |
1114 | - self._rsp = self._make('audioresample', {'quality': 10}) |
1115 | - self._rate = self._make('audiorate') |
1116 | - |
1117 | - # Link elements: |
1118 | - self._q1.link(self._conv) |
1119 | - self._conv.link(self._rsp) |
1120 | - if d.get('caps'): |
1121 | - # FIXME: There is probably a better way to do this, but the caps API |
1122 | - # has always been a bit of a mystery to me. --jderose |
1123 | - if d['enc'] == 'vorbisenc': |
1124 | - mime = 'audio/x-raw-float' |
1125 | - else: |
1126 | - mime = 'audio/x-raw-int' |
1127 | - caps = Gst.caps_from_string( |
1128 | - caps_string(mime, d['caps']) |
1129 | - ) |
1130 | - self._rsp.link_filtered(self._rate, caps) |
1131 | - else: |
1132 | - self._rsp.link(self._rate) |
1133 | - self._rate.link(self._enc) |
1134 | - |
1135 | - |
1136 | -class VideoTranscoder(TranscodeBin): |
1137 | - def __init__(self, d): |
1138 | - super(VideoTranscoder, self).__init__(d) |
1139 | - |
1140 | - # Create processing elements: |
1141 | - self._scale = self._make('ffvideoscale', {'method': 10}) |
1142 | - self._q = self._make('queue') |
1143 | - |
1144 | - # Link elements: |
1145 | - self._q1.link(self._scale) |
1146 | - if d.get('caps'): |
1147 | - caps = Gst.caps_from_string( |
1148 | - caps_string('video/x-raw-yuv', d['caps']) |
1149 | - ) |
1150 | - self._scale.link_filtered(self._q, caps) |
1151 | - else: |
1152 | - self._scale.link(self._q) |
1153 | - self._q.link(self._enc) |
1154 | - |
1155 | - |
1156 | -class Transcoder(object): |
1157 | - def __init__(self, job, fs): |
1158 | - """ |
1159 | - Initialize. |
1160 | - |
1161 | - :param job: a ``dict`` describing the transcode to perform. |
1162 | - :param fs: a `FileStore` instance in which to store transcoded file |
1163 | - """ |
1164 | - if not isinstance(job, dict): |
1165 | - raise TypeError( |
1166 | - TYPE_ERROR % ('job', dict, type(job), job) |
1167 | - ) |
1168 | - if not isinstance(fs, FileStore): |
1169 | - raise TypeError( |
1170 | - TYPE_ERROR % ('fs', FileStore, type(fs), fs) |
1171 | - ) |
1172 | - self.job = job |
1173 | - self.fs = fs |
1174 | - |
1175 | - src = job['src'] |
1176 | - src_filename = self.fs.path(src['id'], src.get('ext')) |
1177 | - self.dst_fp = self.fs.allocate_for_write(job.get('ext')) |
1178 | - |
1179 | - self.mainloop = GObject.MainLoop() |
1180 | - self.pipeline = Gst.Pipeline() |
1181 | - |
1182 | - # Create bus and connect several handlers |
1183 | - self.bus = self.pipeline.get_bus() |
1184 | - self.bus.add_signal_watch() |
1185 | - self.bus.connect('message::eos', self.on_eos) |
1186 | - self.bus.connect('message::error', self.on_error) |
1187 | - |
1188 | - # Create elements |
1189 | - self.src = Gst.ElementFactory.make('filesrc', None) |
1190 | - self.dec = Gst.ElementFactory.make('decodebin2', None) |
1191 | - self.mux = Gst.ElementFactory.make(job['mux'], None) |
1192 | - self.sink = Gst.ElementFactory.make('fdsink', None) |
1193 | - |
1194 | - # Set properties |
1195 | - self.src.set_property('location', src_filename) |
1196 | - self.sink.set_property('fd', self.dst_fp.fileno()) |
1197 | - |
1198 | - # Connect handler for 'new-decoded-pad' signal |
1199 | - self.dec.connect('new-decoded-pad', self.on_new_decoded_pad) |
1200 | - |
1201 | - # Add elements to pipeline |
1202 | - for el in (self.src, self.dec, self.mux, self.sink): |
1203 | - self.pipeline.add(el) |
1204 | - |
1205 | - # Link *some* elements |
1206 | - # This is completed in self.on_new_decoded_pad() |
1207 | - self.src.link(self.dec) |
1208 | - self.mux.link(self.sink) |
1209 | - |
1210 | - self.audio = None |
1211 | - self.video = None |
1212 | - self.tup = None |
1213 | - |
1214 | - def run(self): |
1215 | - self.pipeline.set_state(Gst.State.PLAYING) |
1216 | - self.mainloop.run() |
1217 | - return self.tup |
1218 | - |
1219 | - def kill(self): |
1220 | - self.pipeline.set_state(Gst.State.NULL) |
1221 | - self.pipeline.get_state() |
1222 | - self.mainloop.quit() |
1223 | - |
1224 | - def link_pad(self, pad, name, key): |
1225 | - if key in self.job: |
1226 | - klass = {'audio': AudioTranscoder, 'video': VideoTranscoder}[key] |
1227 | - el = klass(self.job[key]) |
1228 | - else: |
1229 | - el = Gst.ElementFactory.make('fakesink', None) |
1230 | - self.pipeline.add(el) |
1231 | - log.info('Linking pad %r with %r', name, el) |
1232 | - pad.link(el.get_pad('sink')) |
1233 | - if key in self.job: |
1234 | - el.link(self.mux) |
1235 | - el.set_state(Gst.State.PLAYING) |
1236 | - return el |
1237 | - |
1238 | - def on_new_decoded_pad(self, element, pad, last): |
1239 | - name = pad.get_caps().to_string() |
1240 | - log.debug('new decoded pad: %r', name) |
1241 | - if name.startswith('audio/'): |
1242 | - assert self.audio is None |
1243 | - self.audio = self.link_pad(pad, name, 'audio') |
1244 | - elif name.startswith('video/'): |
1245 | - assert self.video is None |
1246 | - self.video = self.link_pad(pad, name, 'video') |
1247 | - |
1248 | - def on_eos(self, bus, msg): |
1249 | - log.info('eos') |
1250 | - self.kill() |
1251 | - self.dst_fp.close() |
1252 | - fp = open(self.dst_fp.name, 'rb') |
1253 | - self.tup = self.fs.tmp_hash_move(fp, self.job.get('ext')) |
1254 | - |
1255 | - def on_error(self, bus, msg): |
1256 | - error = msg.parse_error()[1] |
1257 | - log.error(error) |
1258 | - self.kill() |
1259 | |
1260 | === removed file 'dmedia/gtk/client.py' |
1261 | --- dmedia/gtk/client.py 2011-04-10 09:55:27 +0000 |
1262 | +++ dmedia/gtk/client.py 1970-01-01 00:00:00 +0000 |
1263 | @@ -1,238 +0,0 @@ |
1264 | -# Authors: |
1265 | -# Jason Gerard DeRose <jderose@novacut.com> |
1266 | -# |
1267 | -# dmedia: distributed media library |
1268 | -# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com> |
1269 | -# |
1270 | -# This file is part of `dmedia`. |
1271 | -# |
1272 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
1273 | -# terms of the GNU Affero General Public License as published by the Free |
1274 | -# Software Foundation, either version 3 of the License, or (at your option) any |
1275 | -# later version. |
1276 | -# |
1277 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
1278 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
1279 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
1280 | -# details. |
1281 | -# |
1282 | -# You should have received a copy of the GNU Affero General Public License along |
1283 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
1284 | - |
1285 | -""" |
1286 | -Convenience wrapper for Python applications talking to dmedia dbus service. |
1287 | -""" |
1288 | - |
1289 | -import dbus |
1290 | -import dbus.mainloop.glib |
1291 | -from gi.repository import GObject |
1292 | -from gi.repository.GObject import TYPE_PYOBJECT |
1293 | -from dmedia.constants import IMPORT_BUS, IMPORT_IFACE, EXTENSIONS |
1294 | - |
1295 | - |
1296 | -# We need mainloop integration to test signals: |
1297 | -dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) |
1298 | - |
1299 | - |
1300 | -class Client(GObject.GObject): |
1301 | - """ |
1302 | - Simple and Pythonic way to control dmedia dbus service. |
1303 | - |
1304 | - For Python applications, this client provides several advantages over |
1305 | - strait dbus because it: |
1306 | - |
1307 | - 1. Lazily starts the dmedia service the first time you call a dbus method |
1308 | - or connect a signal handler |
1309 | - |
1310 | - 2. More Pythonic API, including default argument values where they make |
1311 | - sense |
1312 | - |
1313 | - 3. Can use convenient GObject signals |
1314 | - |
1315 | - Controlling import operations |
1316 | - ============================= |
1317 | - |
1318 | - The dmedia service can have multiple import operations running at once. |
1319 | - Import jobs are identified by the path of the directory being imported. |
1320 | - |
1321 | - For example, use `Client.list_imports()` to get the list of currently running |
1322 | - imports: |
1323 | - |
1324 | - >>> from dmedia.gtkui.client import Client |
1325 | - >>> client = Client() #doctest: +SKIP |
1326 | - >>> client.list_imports() #doctest: +SKIP |
1327 | - [] |
1328 | - |
1329 | - Start an import operation using `Client.start_import()`, after which you |
1330 | - will see it in the list of running imports: |
1331 | - |
1332 | - >>> client.start_import('/media/EOS_DIGITAL') #doctest: +SKIP |
1333 | - 'started' |
1334 | - >>> client.list_imports() #doctest: +SKIP |
1335 | - ['/media/EOS_DIGITAL'] |
1336 | - |
1337 | - If you try to import a path for which an import operation is already in |
1338 | - progress, `Client.start_import()` will return the status string |
1339 | - ``'already_running'``: |
1340 | - |
1341 | - >>> client.start_import('/media/EOS_DIGITAL') #doctest: +SKIP |
1342 | - 'already_running' |
1343 | - |
1344 | - Stop an import operation using `Client.stop_import()`, after which there |
1345 | - will be no running imports: |
1346 | - |
1347 | - >>> client.stop_import('/media/EOS_DIGITAL') #doctest: +SKIP |
1348 | - 'stopped' |
1349 | - >>> client.list_imports() #doctest: +SKIP |
1350 | - [] |
1351 | - |
1352 | - If you try to stop an import operation that doesn't exist, |
1353 | - `Client.stop_import()` will return the status string ``'not_running'``: |
1354 | - |
1355 | - >>> client.stop_import('/media/EOS_DIGITAL') #doctest: +SKIP |
1356 | - 'not_running' |
1357 | - |
1358 | - Finally, you can shutdown the dmedia service with `Client.kill()`: |
1359 | - |
1360 | - >>> client.kill() #doctest: +SKIP |
1361 | - |
1362 | - """ |
1363 | - |
1364 | - __gsignals__ = { |
1365 | - 'batch_started': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
1366 | - [TYPE_PYOBJECT] |
1367 | - ), |
1368 | - 'batch_finished': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
1369 | - [TYPE_PYOBJECT, TYPE_PYOBJECT] |
1370 | - ), |
1371 | - 'import_started': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
1372 | - [TYPE_PYOBJECT, TYPE_PYOBJECT] |
1373 | - ), |
1374 | - 'import_count': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
1375 | - [TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT] |
1376 | - ), |
1377 | - 'import_progress': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
1378 | - [TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT, |
1379 | - TYPE_PYOBJECT] |
1380 | - ), |
1381 | - 'import_finished': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
1382 | - [TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT] |
1383 | - ), |
1384 | - } |
1385 | - |
1386 | - def __init__(self, bus=None): |
1387 | - super(Client, self).__init__() |
1388 | - self._bus = (IMPORT_BUS if bus is None else bus) |
1389 | - self._conn = dbus.SessionBus() |
1390 | - self._proxy = None |
1391 | - |
1392 | - @property |
1393 | - def proxy(self): |
1394 | - """ |
1395 | - Lazily create proxy object so dmedia service starts only when needed. |
1396 | - """ |
1397 | - if self._proxy is None: |
1398 | - self._proxy = self._conn.get_object(self._bus, '/') |
1399 | - self._connect_signals() |
1400 | - return self._proxy |
1401 | - |
1402 | - def _call(self, name, *args): |
1403 | - method = self.proxy.get_dbus_method(name, dbus_interface=IMPORT_IFACE) |
1404 | - return method(*args) |
1405 | - |
1406 | - def _connect_signals(self): |
1407 | - self.proxy.connect_to_signal( |
1408 | - 'BatchStarted', self._on_BatchStarted, IMPORT_IFACE |
1409 | - ) |
1410 | - self.proxy.connect_to_signal( |
1411 | - 'BatchFinished', self._on_BatchFinished, IMPORT_IFACE |
1412 | - ) |
1413 | - self.proxy.connect_to_signal( |
1414 | - 'ImportStarted', self._on_ImportStarted, IMPORT_IFACE |
1415 | - ) |
1416 | - self.proxy.connect_to_signal( |
1417 | - 'ImportCount', self._on_ImportCount, IMPORT_IFACE |
1418 | - ) |
1419 | - self.proxy.connect_to_signal( |
1420 | - 'ImportProgress', self._on_ImportProgress, IMPORT_IFACE |
1421 | - ) |
1422 | - self.proxy.connect_to_signal( |
1423 | - 'ImportFinished', self._on_ImportFinished, IMPORT_IFACE |
1424 | - ) |
1425 | - |
1426 | - def _on_BatchStarted(self, batch_id): |
1427 | - self.emit('batch_started', batch_id) |
1428 | - |
1429 | - def _on_BatchFinished(self, batch_id, stats): |
1430 | - self.emit('batch_finished', batch_id, stats) |
1431 | - |
1432 | - def _on_ImportStarted(self, base, import_id): |
1433 | - self.emit('import_started', base, import_id) |
1434 | - |
1435 | - def _on_ImportCount(self, base, import_id, total): |
1436 | - self.emit('import_count', base, import_id, total) |
1437 | - |
1438 | - def _on_ImportProgress(self, base, import_id, completed, total, info): |
1439 | - self.emit('import_progress', base, import_id, completed, total, info) |
1440 | - |
1441 | - def _on_ImportFinished(self, base, import_id, stats): |
1442 | - self.emit('import_finished', base, import_id, stats) |
1443 | - |
1444 | - def connect(self, *args, **kw): |
1445 | - super(Client, self).connect(*args, **kw) |
1446 | - if self._proxy is None: |
1447 | - self.proxy |
1448 | - |
1449 | - def kill(self): |
1450 | - """ |
1451 | - Shutdown the dmedia daemon. |
1452 | - """ |
1453 | - self._call('Kill') |
1454 | - self._proxy = None |
1455 | - |
1456 | - def version(self): |
1457 | - """ |
1458 | - Return version number of running dmedia daemon. |
1459 | - """ |
1460 | - return self._call('Version') |
1461 | - |
1462 | - def get_extensions(self, types): |
1463 | - """ |
1464 | - Get a list of extensions based on broad categories in *types*. |
1465 | - |
1466 | - Currently recognized categories include ``'video'``, ``'audio'``, |
1467 | - ``'images'``, and ``'all'``. You can safely include categories that |
1468 | - don't yet exist. |
1469 | - |
1470 | - :param types: A list of general categories, e.g. ``['video', 'audio']`` |
1471 | - """ |
1472 | - return self._call('GetExtensions', types) |
1473 | - |
1474 | - def start_import(self, base, extract=True): |
1475 | - """ |
1476 | - Start import of card mounted at *base*. |
1477 | - |
1478 | - If *extract* is ``True`` (the default), metadata will be extracted and |
1479 | - thumbnails generated. |
1480 | - |
1481 | - :param base: File-system path from which to import, e.g. |
1482 | - ``'/media/EOS_DIGITAL'`` |
1483 | - :param extract: If ``True``, perform metadata extraction, thumbnail |
1484 | - generation; default is ``True``. |
1485 | - """ |
1486 | - return self._call('StartImport', base, extract) |
1487 | - |
1488 | - def stop_import(self, base): |
1489 | - """ |
1490 | - Start import of card mounted at *base*. |
1491 | - |
1492 | - :param base: File-system path from which to import, e.g. |
1493 | - ``'/media/EOS_DIGITAL'`` |
1494 | - """ |
1495 | - return self._call('StopImport', base) |
1496 | - |
1497 | - def list_imports(self): |
1498 | - """ |
1499 | - Return list of currently running imports. |
1500 | - """ |
1501 | - return self._call('ListImports') |
1502 | |
1503 | === removed file 'dmedia/gtk/firstrun.py' |
1504 | --- dmedia/gtk/firstrun.py 2011-03-28 11:25:27 +0000 |
1505 | +++ dmedia/gtk/firstrun.py 1970-01-01 00:00:00 +0000 |
1506 | @@ -1,279 +0,0 @@ |
1507 | -#!/usr/bin/env python |
1508 | - |
1509 | -# Authors: |
1510 | -# David Green <david4dev@gmail.com> |
1511 | -# Jason Gerard DeRose <jderose@novacut.com> |
1512 | -# |
1513 | -# dmedia: distributed media library |
1514 | -# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com> |
1515 | -# |
1516 | -# This file is part of `dmedia`. |
1517 | -# |
1518 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
1519 | -# terms of the GNU Affero General Public License as published by the Free |
1520 | -# Software Foundation, either version 3 of the License, or (at your option) any |
1521 | -# later version. |
1522 | -# |
1523 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
1524 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
1525 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
1526 | -# details. |
1527 | -# |
1528 | -# You should have received a copy of the GNU Affero General Public License along |
1529 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
1530 | - |
1531 | -import os |
1532 | -from os import path |
1533 | -from xml.sax import saxutils |
1534 | -from gettext import gettext as _ |
1535 | - |
1536 | -from gi.repository import Gtk, Pango, GConf |
1537 | - |
1538 | - |
1539 | -NO_DIALOG = '/apps/dmedia/dont-show-import-firstrun' |
1540 | -conf = GConf.Client.get_default() |
1541 | - |
1542 | - |
1543 | -TITLE = _('DMedia Importer') |
1544 | - |
1545 | -ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR |
1546 | -PADDING = 5 |
1547 | - |
1548 | - |
1549 | -class EasyBox(object): |
1550 | - """ |
1551 | - A more convenient box borrowed from TymeLapse. |
1552 | - """ |
1553 | - def _pack(self, widget, expand=False, padding=PADDING, end=False): |
1554 | - method = (self.pack_start if not end else self.pack_end) |
1555 | - method(widget, expand, expand, padding) |
1556 | - return widget |
1557 | - |
1558 | - |
1559 | -class VBox(Gtk.VBox, EasyBox): |
1560 | - pass |
1561 | - |
1562 | - |
1563 | -class HBox(Gtk.HBox, EasyBox): |
1564 | - pass |
1565 | - |
1566 | - |
1567 | -class Label(Gtk.Label): |
1568 | - """ |
1569 | - A more convenient label borrowed from TymeLapse. |
1570 | - """ |
1571 | - def __init__(self, text, *tags): |
1572 | - super(Label, self).__init__() |
1573 | - self.set_alignment(0, 0.5) |
1574 | - self.set_padding(5, 5) |
1575 | - self._text = text |
1576 | - self._tags = set(tags) |
1577 | - self._update() |
1578 | - #self.set_selectable(True) |
1579 | - |
1580 | - def _add_tags(self, *tags): |
1581 | - self._tags.update(tags) |
1582 | - self._update() |
1583 | - |
1584 | - def _remove_tags(self, *tags): |
1585 | - for tag in tags: |
1586 | - if tag in self._tags: |
1587 | - self._tags.remove(tag) |
1588 | - self._update() |
1589 | - |
1590 | - def _set_text(self, text): |
1591 | - self._text = text |
1592 | - self._update() |
1593 | - |
1594 | - def _update(self): |
1595 | - text = ('' if self._text is None else self._text) |
1596 | - if text and self._tags: |
1597 | - m = saxutils.escape(text) |
1598 | - for tag in self._tags: |
1599 | - m = '<%(tag)s>%(m)s</%(tag)s>' % dict(tag=tag, m=m) |
1600 | - self.set_markup(m) |
1601 | - else: |
1602 | - self.set_text(text) |
1603 | - |
1604 | - |
1605 | -class Button(Gtk.Button): |
1606 | - def __init__(self, stock=None, text=None): |
1607 | - super(Button, self).__init__() |
1608 | - hbox = HBox() |
1609 | - self.add(hbox) |
1610 | - self._image = Gtk.Image() |
1611 | - self._label = Label(None) |
1612 | - hbox._pack(self._image) |
1613 | - hbox._pack(self._label, expand=True) |
1614 | - if stock is not None: |
1615 | - self._set_stock(stock) |
1616 | - if text is not None: |
1617 | - self._set_text(text) |
1618 | - |
1619 | - def _set_stock(self, stock, size=ICON_SIZE): |
1620 | - self._image.set_from_stock(stock, size) |
1621 | - |
1622 | - def _set_text(self, text): |
1623 | - self._label.set_text(text) |
1624 | - |
1625 | - |
1626 | -class FolderChooser(Button): |
1627 | - def __init__(self): |
1628 | - super(FolderChooser, self).__init__(stock=Gtk.STOCK_OPEN) |
1629 | - self._label.set_ellipsize(Pango.EllipsizeMode.START) |
1630 | - self._title = _('Choose folder to import...') |
1631 | - self.connect('clicked', self._on_clicked) |
1632 | - self._set_value(os.environ['HOME']) |
1633 | - |
1634 | - def _set_value(self, value): |
1635 | - self._value = path.abspath(value) |
1636 | - self._label._set_text(self._value) |
1637 | - |
1638 | - def _on_clicked(self, button): |
1639 | - dialog = Gtk.FileChooserDialog( |
1640 | - title=self._title, |
1641 | - action=Gtk.FileChooserAction.SELECT_FOLDER, |
1642 | - buttons=( |
1643 | - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, |
1644 | - Gtk.STOCK_OPEN, Gtk.ResponseType.OK, |
1645 | - ), |
1646 | - ) |
1647 | - dialog.set_current_folder(self._value) |
1648 | - response = dialog.run() |
1649 | - if response == Gtk.ResponseType.OK: |
1650 | - self._set_value(dialog.get_filename()) |
1651 | - dialog.destroy() |
1652 | - |
1653 | - |
1654 | -def okay_button(): |
1655 | - return Button(Gtk.STOCK_OK, _('OK, Import Files')) |
1656 | - |
1657 | - |
1658 | -class ImportDialog(Gtk.Window): |
1659 | - def __init__(self): |
1660 | - super(ImportDialog, self).__init__() |
1661 | - self.set_default_size(425, 200) |
1662 | - self.set_title(TITLE) |
1663 | - try: |
1664 | - self.set_icon_from_file('/usr/share/pixmaps/dmedia.svg') |
1665 | - except: |
1666 | - pass |
1667 | - self.connect('destroy', Gtk.main_quit) |
1668 | - |
1669 | - self._value = None |
1670 | - |
1671 | - hbox = HBox() |
1672 | - self.add(hbox) |
1673 | - |
1674 | - vbox = VBox() |
1675 | - hbox._pack(vbox, expand=True) |
1676 | - |
1677 | - vbox._pack(Label(_('Choose Folder:'), 'b')) |
1678 | - self._folder = FolderChooser() |
1679 | - vbox._pack(self._folder) |
1680 | - |
1681 | - self._button = okay_button() |
1682 | - vbox._pack(self._button, end=True) |
1683 | - self._button.connect('clicked', self._on_clicked) |
1684 | - |
1685 | - self.show_all() |
1686 | - |
1687 | - def run(self): |
1688 | - Gtk.main() |
1689 | - return self._value |
1690 | - |
1691 | - def _on_clicked(self, button): |
1692 | - self._value = self._folder._value |
1693 | - Gtk.main_quit() |
1694 | - |
1695 | - |
1696 | -class FirstRunGUI(Gtk.Window): |
1697 | - """ |
1698 | - Promt use first time dmedia importer is run. |
1699 | - |
1700 | - For example: |
1701 | - |
1702 | - >>> from dmedia.gtkui.firstrun import FirstRunGUI |
1703 | - >>> run = FirstRunGUI.run_if_first_run('/media/EOS_DIGITAL') #doctest: +SKIP |
1704 | - """ |
1705 | - |
1706 | - def __init__(self): |
1707 | - super(FirstRunGUI, self).__init__() |
1708 | - self.set_default_size(425, 200) |
1709 | - |
1710 | - self.set_title(TITLE) |
1711 | - try: |
1712 | - self.set_icon_from_file('/usr/share/pixmaps/dmedia.svg') |
1713 | - except: |
1714 | - pass |
1715 | - |
1716 | - self.ok_was_pressed = False |
1717 | - |
1718 | - self.add_content() |
1719 | - |
1720 | - self.connect_signals() |
1721 | - |
1722 | - def add_content(self): |
1723 | - vbox = VBox() |
1724 | - self.add(vbox) |
1725 | - |
1726 | - label1 = Label(_('Welcome to the dmedia importer!'), 'big') |
1727 | - label1.set_alignment(0.5, 0.5) |
1728 | - vbox._pack(label1) |
1729 | - |
1730 | - label2 = Label(_('It will import all files in the following folder:')) |
1731 | - vbox._pack(label2) |
1732 | - |
1733 | - self.folder = Label(None, 'b') |
1734 | - self.folder.set_ellipsize(Pango.EllipsizeMode.START) |
1735 | - vbox._pack(self.folder) |
1736 | - |
1737 | - self.dont_show_again = Gtk.CheckButton(label=_("Don't show this again")) |
1738 | - self.dont_show_again.set_active(True) |
1739 | - |
1740 | - self.ok = okay_button() |
1741 | - |
1742 | - hbox = HBox() |
1743 | - hbox._pack(self.ok, expand=True) |
1744 | - hbox._pack(self.dont_show_again) |
1745 | - |
1746 | - vbox._pack(hbox, end=True) |
1747 | - |
1748 | - |
1749 | - def connect_signals(self): |
1750 | - self.connect("delete_event", self.delete_event) |
1751 | - self.connect("destroy", self.destroy) |
1752 | - self.ok.connect("clicked", self.on_ok) |
1753 | - |
1754 | - |
1755 | - def destroy(self, widget, data=None): |
1756 | - Gtk.main_quit() |
1757 | - |
1758 | - |
1759 | - def delete_event(self, widget, event, data=None): |
1760 | - return False |
1761 | - |
1762 | - |
1763 | - def on_ok(self, widget): |
1764 | - dont_show_again = self.dont_show_again.get_active() |
1765 | - val = 0 |
1766 | - if dont_show_again: |
1767 | - val = 1 |
1768 | - conf.set_bool(NO_DIALOG, val) |
1769 | - self.ok_was_pressed = True |
1770 | - self.destroy(widget) |
1771 | - |
1772 | - def go(self, folder): |
1773 | - self.folder._set_text(folder) |
1774 | - self.show_all() |
1775 | - Gtk.main() |
1776 | - return self.ok_was_pressed |
1777 | - |
1778 | - @classmethod |
1779 | - def run_if_first_run(cls, base, unset=False): |
1780 | - if unset: |
1781 | - conf.unset(NO_DIALOG) |
1782 | - if conf.get_bool(NO_DIALOG): |
1783 | - return True |
1784 | - app = cls() |
1785 | - return app.go(base) |
1786 | |
1787 | === removed file 'dmedia/gtk/menu.py' |
1788 | --- dmedia/gtk/menu.py 2011-04-17 03:10:38 +0000 |
1789 | +++ dmedia/gtk/menu.py 1970-01-01 00:00:00 +0000 |
1790 | @@ -1,408 +0,0 @@ |
1791 | -# Authors: |
1792 | -# David Green <david4dev@gmail.com> |
1793 | -# |
1794 | -# dmedia: distributed media library |
1795 | -# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com> |
1796 | -# |
1797 | -# This file is part of `dmedia`. |
1798 | -# |
1799 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
1800 | -# terms of the GNU Affero General Public License as published by the Free |
1801 | -# Software Foundation, either version 3 of the License, or (at your option) any |
1802 | -# later version. |
1803 | -# |
1804 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
1805 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
1806 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
1807 | -# details. |
1808 | -# |
1809 | -# You should have received a copy of the GNU Affero General Public License along |
1810 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
1811 | - |
1812 | -#TODO: separators, keyboard acceleration |
1813 | - |
1814 | -""" |
1815 | -Menu declaration. |
1816 | -""" |
1817 | - |
1818 | - |
1819 | -from gi.repository import Gtk |
1820 | - |
1821 | -ACTIONS = { |
1822 | - "close" : Gtk.main_quit |
1823 | -} |
1824 | - |
1825 | - |
1826 | -#this could potentially be put into a .json file |
1827 | -MENU = [ |
1828 | - { |
1829 | - "label" : "_File", |
1830 | - "type" : "menu", |
1831 | - "items" : [ |
1832 | - { |
1833 | - "label" : "_Close", |
1834 | - "type" : "action", |
1835 | - "action" : "close" |
1836 | - } |
1837 | - ] |
1838 | - } |
1839 | -] |
1840 | - |
1841 | -#A menu for testing and demonstration |
1842 | -TEST_MENU = [ |
1843 | -{ |
1844 | - "label" : "_File", |
1845 | - "type" : "menu", |
1846 | - "items" : [ |
1847 | - { |
1848 | - "label" : "_Close", |
1849 | - "type" : "action", |
1850 | - "action" : "close" |
1851 | - }, |
1852 | - { |
1853 | - "label" : "_Close", |
1854 | - "type" : "action", |
1855 | - "action" : "close" |
1856 | - }, |
1857 | - { |
1858 | - "type" : "custom", |
1859 | - "widget" : Gtk.SeparatorMenuItem() |
1860 | - }, |
1861 | - { |
1862 | - "label" : "_Close", |
1863 | - "type" : "menu", |
1864 | - "items" : [{ |
1865 | - "label" : "_Close", |
1866 | - "type" : "action", |
1867 | - "action" : "close" |
1868 | - }, |
1869 | - { |
1870 | - "label" : "_Close", |
1871 | - "type" : "action", |
1872 | - "action" : "close" |
1873 | - }, |
1874 | - { |
1875 | - "type" : "custom", |
1876 | - "widget" : Gtk.SeparatorMenuItem() |
1877 | - }, |
1878 | - { |
1879 | - "label" : "_Close", |
1880 | - "type" : "menu", |
1881 | - "items" : [{ |
1882 | - "label" : "_Close", |
1883 | - "type" : "action", |
1884 | - "action" : "close" |
1885 | - }, |
1886 | - { |
1887 | - "label" : "_Close", |
1888 | - "type" : "action", |
1889 | - "action" : "close" |
1890 | - }, |
1891 | - { |
1892 | - "type" : "custom", |
1893 | - "widget" : Gtk.SeparatorMenuItem() |
1894 | - }, |
1895 | - { |
1896 | - "label" : "_Close", |
1897 | - "type" : "action", |
1898 | - "action" : "close" |
1899 | - }] |
1900 | - }, |
1901 | - { |
1902 | - "label" : "_Close", |
1903 | - "type" : "action", |
1904 | - "action" : "close" |
1905 | - }] |
1906 | - }, |
1907 | - { |
1908 | - "label" : "_Close", |
1909 | - "type" : "action", |
1910 | - "action" : "close" |
1911 | - } |
1912 | - ] |
1913 | - }, |
1914 | - { |
1915 | - "label" : "_Edit", |
1916 | - "type" : "menu", |
1917 | - "items" : [ |
1918 | - { |
1919 | - "label" : "_Close", |
1920 | - "type" : "action", |
1921 | - "action" : "close" |
1922 | - }, |
1923 | - { |
1924 | - "label" : "_Close", |
1925 | - "type" : "action", |
1926 | - "action" : "close" |
1927 | - }, |
1928 | - { |
1929 | - "type" : "custom", |
1930 | - "widget" : Gtk.SeparatorMenuItem() |
1931 | - }, |
1932 | - { |
1933 | - "label" : "_Close", |
1934 | - "type" : "menu", |
1935 | - "items" : [{ |
1936 | - "label" : "_Close", |
1937 | - "type" : "action", |
1938 | - "action" : "close" |
1939 | - }, |
1940 | - { |
1941 | - "label" : "_Close", |
1942 | - "type" : "action", |
1943 | - "action" : "close" |
1944 | - }, |
1945 | - { |
1946 | - "type" : "custom", |
1947 | - "widget" : Gtk.SeparatorMenuItem() |
1948 | - }, |
1949 | - { |
1950 | - "label" : "_Close", |
1951 | - "type" : "menu", |
1952 | - "items" : [{ |
1953 | - "label" : "_Close", |
1954 | - "type" : "action", |
1955 | - "action" : "close" |
1956 | - }, |
1957 | - { |
1958 | - "label" : "_Close", |
1959 | - "type" : "action", |
1960 | - "action" : "close" |
1961 | - }, |
1962 | - { |
1963 | - "type" : "custom", |
1964 | - "widget" : Gtk.SeparatorMenuItem() |
1965 | - }, |
1966 | - { |
1967 | - "label" : "_Close", |
1968 | - "type" : "action", |
1969 | - "action" : "close" |
1970 | - }] |
1971 | - }, |
1972 | - { |
1973 | - "label" : "_Close", |
1974 | - "type" : "action", |
1975 | - "action" : "close" |
1976 | - }] |
1977 | - }, |
1978 | - { |
1979 | - "label" : "_Close", |
1980 | - "type" : "action", |
1981 | - "action" : "close" |
1982 | - } |
1983 | - ] |
1984 | - }, |
1985 | - { |
1986 | - "label" : "_View", |
1987 | - "type" : "menu", |
1988 | - "items" : [ |
1989 | - { |
1990 | - "label" : "_Close", |
1991 | - "type" : "action", |
1992 | - "action" : "close" |
1993 | - }, |
1994 | - { |
1995 | - "label" : "_Close", |
1996 | - "type" : "action", |
1997 | - "action" : "close" |
1998 | - }, |
1999 | - { |
2000 | - "type" : "custom", |
2001 | - "widget" : Gtk.SeparatorMenuItem() |
2002 | - }, |
2003 | - { |
2004 | - "label" : "_Close", |
2005 | - "type" : "menu", |
2006 | - "items" : [{ |
2007 | - "label" : "_Close", |
2008 | - "type" : "action", |
2009 | - "action" : "close" |
2010 | - }, |
2011 | - { |
2012 | - "label" : "_Close", |
2013 | - "type" : "action", |
2014 | - "action" : "close" |
2015 | - }, |
2016 | - { |
2017 | - "type" : "custom", |
2018 | - "widget" : Gtk.SeparatorMenuItem() |
2019 | - }, |
2020 | - { |
2021 | - "label" : "_Close", |
2022 | - "type" : "menu", |
2023 | - "items" : [{ |
2024 | - "label" : "_Close", |
2025 | - "type" : "action", |
2026 | - "action" : "close" |
2027 | - }, |
2028 | - { |
2029 | - "label" : "_Close", |
2030 | - "type" : "action", |
2031 | - "action" : "close" |
2032 | - }, |
2033 | - { |
2034 | - "type" : "custom", |
2035 | - "widget" : Gtk.SeparatorMenuItem() |
2036 | - }, |
2037 | - { |
2038 | - "label" : "_Close", |
2039 | - "type" : "action", |
2040 | - "action" : "close" |
2041 | - }] |
2042 | - }, |
2043 | - { |
2044 | - "label" : "_Close", |
2045 | - "type" : "action", |
2046 | - "action" : "close" |
2047 | - }] |
2048 | - }, |
2049 | - { |
2050 | - "label" : "_Close", |
2051 | - "type" : "action", |
2052 | - "action" : "close" |
2053 | - } |
2054 | - ] |
2055 | - }, |
2056 | - { |
2057 | - "label" : "_Tools", |
2058 | - "type" : "menu", |
2059 | - "items" : [ |
2060 | - { |
2061 | - "label" : "_Close", |
2062 | - "type" : "action", |
2063 | - "action" : "close" |
2064 | - }, |
2065 | - { |
2066 | - "label" : "_Close", |
2067 | - "type" : "action", |
2068 | - "action" : "close" |
2069 | - }, |
2070 | - { |
2071 | - "type" : "custom", |
2072 | - "widget" : Gtk.SeparatorMenuItem() |
2073 | - }, |
2074 | - { |
2075 | - "label" : "_Close", |
2076 | - "type" : "menu", |
2077 | - "items" : [{ |
2078 | - "label" : "_Close", |
2079 | - "type" : "action", |
2080 | - "action" : "close" |
2081 | - }, |
2082 | - { |
2083 | - "label" : "_Close", |
2084 | - "type" : "action", |
2085 | - "action" : "close" |
2086 | - }, |
2087 | - { |
2088 | - "type" : "custom", |
2089 | - "widget" : Gtk.SeparatorMenuItem() |
2090 | - }, |
2091 | - { |
2092 | - "label" : "_Close", |
2093 | - "type" : "menu", |
2094 | - "items" : [{ |
2095 | - "label" : "_Close", |
2096 | - "type" : "action", |
2097 | - "action" : "close" |
2098 | - }, |
2099 | - { |
2100 | - "label" : "_Close", |
2101 | - "type" : "action", |
2102 | - "action" : "close" |
2103 | - }, |
2104 | - { |
2105 | - "type" : "custom", |
2106 | - "widget" : Gtk.SeparatorMenuItem() |
2107 | - }, |
2108 | - { |
2109 | - "label" : "_Close", |
2110 | - "type" : "action", |
2111 | - "action" : "close" |
2112 | - }] |
2113 | - }, |
2114 | - { |
2115 | - "label" : "_Close", |
2116 | - "type" : "action", |
2117 | - "action" : "close" |
2118 | - }] |
2119 | - }, |
2120 | - { |
2121 | - "label" : "_Close", |
2122 | - "type" : "action", |
2123 | - "action" : "close" |
2124 | - } |
2125 | - ] |
2126 | - }, |
2127 | - { |
2128 | - "label" : "Et_c", |
2129 | - "type" : "menu", |
2130 | - "items" : [ |
2131 | - { |
2132 | - "label" : "_Close", |
2133 | - "type" : "action", |
2134 | - "action" : "close" |
2135 | - }, |
2136 | - { |
2137 | - "label" : "_Close", |
2138 | - "type" : "action", |
2139 | - "action" : "close" |
2140 | - }, |
2141 | - { |
2142 | - "type" : "custom", |
2143 | - "widget" : Gtk.SeparatorMenuItem() |
2144 | - }, |
2145 | - { |
2146 | - "label" : "_Close", |
2147 | - "type" : "menu", |
2148 | - "items" : [{ |
2149 | - "label" : "_Close", |
2150 | - "type" : "action", |
2151 | - "action" : "close" |
2152 | - }, |
2153 | - { |
2154 | - "label" : "_Close", |
2155 | - "type" : "action", |
2156 | - "action" : "close" |
2157 | - }, |
2158 | - { |
2159 | - "type" : "custom", |
2160 | - "widget" : Gtk.SeparatorMenuItem() |
2161 | - }, |
2162 | - { |
2163 | - "label" : "_Close", |
2164 | - "type" : "menu", |
2165 | - "items" : [{ |
2166 | - "label" : "_Close", |
2167 | - "type" : "action", |
2168 | - "action" : "close" |
2169 | - }, |
2170 | - { |
2171 | - "label" : "_Close", |
2172 | - "type" : "action", |
2173 | - "action" : "close" |
2174 | - }, |
2175 | - { |
2176 | - "type" : "custom", |
2177 | - "widget" : Gtk.SeparatorMenuItem() |
2178 | - }, |
2179 | - { |
2180 | - "label" : "_Close", |
2181 | - "type" : "action", |
2182 | - "action" : "close" |
2183 | - }] |
2184 | - }, |
2185 | - { |
2186 | - "label" : "_Close", |
2187 | - "type" : "action", |
2188 | - "action" : "close" |
2189 | - }] |
2190 | - }, |
2191 | - { |
2192 | - "label" : "_Close", |
2193 | - "type" : "action", |
2194 | - "action" : "close" |
2195 | - } |
2196 | - ] |
2197 | - } |
2198 | -] |
2199 | |
2200 | === removed file 'dmedia/gtk/service.py' |
2201 | --- dmedia/gtk/service.py 2011-09-04 00:53:41 +0000 |
2202 | +++ dmedia/gtk/service.py 1970-01-01 00:00:00 +0000 |
2203 | @@ -1,302 +0,0 @@ |
2204 | -# Authors: |
2205 | -# Jason Gerard DeRose <jderose@novacut.com> |
2206 | -# Manish SInha <mail@manishsinha.net> |
2207 | -# David Green <david4dev@gmail.com> |
2208 | -# |
2209 | -# dmedia: distributed media library |
2210 | -# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com> |
2211 | -# |
2212 | -# This file is part of `dmedia`. |
2213 | -# |
2214 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
2215 | -# terms of the GNU Affero General Public License as published by the Free |
2216 | -# Software Foundation, either version 3 of the License, or (at your option) any |
2217 | -# later version. |
2218 | -# |
2219 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
2220 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
2221 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
2222 | -# details. |
2223 | -# |
2224 | -# You should have received a copy of the GNU Affero General Public License along |
2225 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
2226 | - |
2227 | -""" |
2228 | -D-Bus service implementing Pro File Import UX. |
2229 | - |
2230 | -For details, please see: |
2231 | - |
2232 | - https://wiki.ubuntu.com/AyatanaDmediaLovefest |
2233 | -""" |
2234 | - |
2235 | -from os import path |
2236 | -from gettext import gettext as _ |
2237 | -import logging |
2238 | -from subprocess import check_call |
2239 | - |
2240 | -import dbus |
2241 | -import dbus.service |
2242 | -import dbus.mainloop.glib |
2243 | - |
2244 | -from dmedia import __version__ |
2245 | -from dmedia.constants import IMPORT_BUS, IMPORT_IFACE, EXT_MAP |
2246 | -from dmedia.importer import ImportManager |
2247 | - |
2248 | -from .util import NotifyManager, Timer, import_started, batch_finished |
2249 | - |
2250 | - |
2251 | -try: |
2252 | - from gi.repository import Notify |
2253 | - Notify.init('dmedia') |
2254 | -except ImportError: |
2255 | - Notify = None |
2256 | - |
2257 | -try: |
2258 | - from gi.repository import AppIndicator3 |
2259 | - from gi.repository import Gtk |
2260 | -except ImportError: |
2261 | - AppIndicator3 = None |
2262 | - |
2263 | -log = logging.getLogger() |
2264 | - |
2265 | - |
2266 | -ICON = 'indicator-rendermenu' |
2267 | -ICON_ATT = 'indicator-rendermenu-att' |
2268 | - |
2269 | - |
2270 | -class DMedia(dbus.service.Object): |
2271 | - __signals = frozenset([ |
2272 | - 'BatchStarted', |
2273 | - 'BatchFinished', |
2274 | - 'ImportStarted', |
2275 | - 'ImportCount', |
2276 | - 'ImportProgress', |
2277 | - 'ImportFinished', |
2278 | - ]) |
2279 | - |
2280 | - def __init__(self, env, bus, killfunc): |
2281 | - assert callable(killfunc) |
2282 | - self._env = env |
2283 | - self._bus = bus |
2284 | - self._killfunc = killfunc |
2285 | - self._bus = bus |
2286 | - self._no_gui = env.get('no_gui', False) |
2287 | - log.info('Starting service on %r', self._bus) |
2288 | - self._conn = dbus.SessionBus() |
2289 | - super(DMedia, self).__init__(self._conn, object_path='/') |
2290 | - self._busname = dbus.service.BusName(self._bus, self._conn) |
2291 | - |
2292 | - if self._no_gui or Notify is None: |
2293 | - self._notify = None |
2294 | - else: |
2295 | - log.info('Using `Notify`') |
2296 | - self._notify = NotifyManager() |
2297 | - |
2298 | - if self._no_gui or AppIndicator3 is None: |
2299 | - self._indicator = None |
2300 | - else: |
2301 | - log.info('Using `AppIndicator3`') |
2302 | - self._indicator = AppIndicator3.Indicator.new('rendermenu', ICON, |
2303 | - AppIndicator3.IndicatorCategory.APPLICATION_STATUS |
2304 | - ) |
2305 | - self._timer = Timer(2, self._on_timer) |
2306 | - self._indicator.set_attention_icon(ICON_ATT) |
2307 | - self._menu = Gtk.Menu() |
2308 | - |
2309 | - self._current = Gtk.MenuItem() |
2310 | - self._menu.append(self._current) |
2311 | - |
2312 | - sep = Gtk.SeparatorMenuItem() |
2313 | - self._menu.append(sep) |
2314 | - |
2315 | - futon = Gtk.MenuItem() |
2316 | - futon.set_label(_('Browse DB in Futon')) |
2317 | - futon.connect('activate', self._on_futon) |
2318 | - self._menu.append(futon) |
2319 | - |
2320 | - quit = Gtk.MenuItem() |
2321 | - quit.set_label(_('Shutdown dmedia')) |
2322 | - quit.connect('activate', self._on_quit) |
2323 | - self._menu.append(quit) |
2324 | - |
2325 | - self._menu.show_all() |
2326 | - self._current.hide() |
2327 | - self._indicator.set_menu(self._menu) |
2328 | - self._indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) |
2329 | - |
2330 | - self._manager = None |
2331 | - |
2332 | - @property |
2333 | - def manager(self): |
2334 | - if self._manager is None: |
2335 | - self._manager = ImportManager(self._env, self._on_signal) |
2336 | - return self._manager |
2337 | - |
2338 | - def _on_signal(self, signal, args): |
2339 | - if signal in self.__signals: |
2340 | - method = getattr(self, signal) |
2341 | - method(*args) |
2342 | - |
2343 | - def _on_timer(self): |
2344 | - if self._manager is None: |
2345 | - return |
2346 | - text = _('File {completed} of {total}').format( |
2347 | - **self._manager.get_batch_progress() |
2348 | - ) |
2349 | - self._current.set_label(text) |
2350 | -# self._indicator.set_menu(self._menu) |
2351 | - |
2352 | - def _on_quit(self, menuitem): |
2353 | - self.Kill() |
2354 | - |
2355 | - def _on_futon(self, menuitem): |
2356 | - log.info('Opening dmedia database in Futon..') |
2357 | - try: |
2358 | - uri = self.metastore.get_auth_uri() + '/_utils' |
2359 | - check_call(['/usr/bin/xdg-open', uri]) |
2360 | - log.info('Opened Futon') |
2361 | - except Exception: |
2362 | - log.exception('Could not open dmedia database in Futon') |
2363 | - |
2364 | - @dbus.service.signal(IMPORT_IFACE, signature='s') |
2365 | - def BatchStarted(self, batch_id): |
2366 | - """ |
2367 | - Fired at transition from idle to at least one active import. |
2368 | - |
2369 | - For pro file import UX, the RenderMenu should be set to STATUS_ATTENTION |
2370 | - when this signal is received. |
2371 | - """ |
2372 | - if self._notify: |
2373 | - self._batch = [] |
2374 | - if self._indicator: |
2375 | - self._indicator.set_status(AppIndicator3.IndicatorStatus.ATTENTION) |
2376 | - self._current.show() |
2377 | - self._current.set_label(_('Searching for files...')) |
2378 | - self._indicator.set_menu(self._menu) |
2379 | - self._timer.start() |
2380 | - |
2381 | - @dbus.service.signal(IMPORT_IFACE, signature='sa{sx}') |
2382 | - def BatchFinished(self, batch_id, stats): |
2383 | - """ |
2384 | - Fired at transition from at least one active import to idle. |
2385 | - |
2386 | - *stats* will be the combined stats of all imports in this batch. |
2387 | - |
2388 | - For pro file import UX, the RenderMenu should be set back to |
2389 | - STATUS_ACTIVE, and the NotifyOSD with the aggregate import stats should |
2390 | - be displayed when this signal is received. |
2391 | - """ |
2392 | - if self._indicator: |
2393 | - self._indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) |
2394 | - if self._notify is None: |
2395 | - return |
2396 | - (summary, body) = batch_finished(stats) |
2397 | - self._notify.replace(summary, body, 'notification-device-eject') |
2398 | - self._timer.stop() |
2399 | - self._current.hide() |
2400 | - |
2401 | - @dbus.service.signal(IMPORT_IFACE, signature='ss') |
2402 | - def ImportStarted(self, base, import_id): |
2403 | - """ |
2404 | - Fired when card is inserted. |
2405 | - |
2406 | - For pro file import UX, the "Searching for new files" NotifyOSD should |
2407 | - be displayed when this signal is received. If a previous notification |
2408 | - is still visible, the two should be merge and the summary conspicuously |
2409 | - changed to be very clear that both cards were detected. |
2410 | - """ |
2411 | - if self._notify is None: |
2412 | - return |
2413 | - self._batch.append(base) |
2414 | - (summary, body) = import_started(self._batch) |
2415 | - # FIXME: use correct icon depending on whether card reader is corrected |
2416 | - # via FireWire or USB |
2417 | - self._notify.replace(summary, body, 'notification-device-usb') |
2418 | - |
2419 | - @dbus.service.signal(IMPORT_IFACE, signature='ssx') |
2420 | - def ImportCount(self, base, import_id, total): |
2421 | - pass |
2422 | - |
2423 | - @dbus.service.signal(IMPORT_IFACE, signature='ssiia{ss}') |
2424 | - def ImportProgress(self, base, import_id, completed, total, info): |
2425 | - pass |
2426 | - |
2427 | - @dbus.service.signal(IMPORT_IFACE, signature='ssa{sx}') |
2428 | - def ImportFinished(self, base, import_id, stats): |
2429 | - pass |
2430 | - |
2431 | - @dbus.service.method(IMPORT_IFACE, in_signature='', out_signature='') |
2432 | - def Kill(self): |
2433 | - """ |
2434 | - Kill the dmedia service process. |
2435 | - """ |
2436 | - log.info('Killing service...') |
2437 | - if self._manager is not None: |
2438 | - self._manager.kill() |
2439 | - if callable(self._killfunc): |
2440 | - log.info('Calling killfunc()') |
2441 | - self._killfunc() |
2442 | - |
2443 | - @dbus.service.method(IMPORT_IFACE, in_signature='', out_signature='s') |
2444 | - def Version(self): |
2445 | - """ |
2446 | - Return dmedia version. |
2447 | - """ |
2448 | - return __version__ |
2449 | - |
2450 | - @dbus.service.method(IMPORT_IFACE, in_signature='as', out_signature='as') |
2451 | - def GetExtensions(self, types): |
2452 | - """ |
2453 | - Get a list of extensions based on broad categories in *types*. |
2454 | - |
2455 | - Currently recognized categories include ``'video'``, ``'audio'``, |
2456 | - ``'images'``, and ``'all'``. You can safely include categories that |
2457 | - don't yet exist. |
2458 | - |
2459 | - :param types: A list of general categories, e.g. ``['video', 'audio']`` |
2460 | - """ |
2461 | - extensions = set() |
2462 | - for key in types: |
2463 | - if key in EXT_MAP: |
2464 | - extensions.update(EXT_MAP[key]) |
2465 | - return sorted(extensions) |
2466 | - |
2467 | - @dbus.service.method(IMPORT_IFACE, in_signature='sb', out_signature='s') |
2468 | - def StartImport(self, base, extract): |
2469 | - """ |
2470 | - Start import of card mounted at *base*. |
2471 | - |
2472 | - If *extract* is ``True``, metadata will be extracted and thumbnails |
2473 | - generated. |
2474 | - |
2475 | - :param base: File-system path from which to import, e.g. |
2476 | - ``'/media/EOS_DIGITAL'`` |
2477 | - :param extract: If ``True``, perform metadata extraction, thumbnail |
2478 | - generation |
2479 | - """ |
2480 | - base = unicode(base) |
2481 | - if path.abspath(base) != base: |
2482 | - return 'not_abspath' |
2483 | - if not path.isdir(base): |
2484 | - return 'not_a_dir' |
2485 | - if self.manager.start_import(base, extract): |
2486 | - return 'started' |
2487 | - return 'already_running' |
2488 | - |
2489 | - @dbus.service.method(IMPORT_IFACE, in_signature='s', out_signature='s') |
2490 | - def StopImport(self, base): |
2491 | - """ |
2492 | - In running, stop the import of directory or file at *base*. |
2493 | - """ |
2494 | - if self.manager.kill_job(base): |
2495 | - return 'stopped' |
2496 | - return 'not_running' |
2497 | - |
2498 | - @dbus.service.method(IMPORT_IFACE, in_signature='', out_signature='as') |
2499 | - def ListImports(self): |
2500 | - """ |
2501 | - Return list of currently running imports. |
2502 | - """ |
2503 | - if self._manager is None: |
2504 | - return [] |
2505 | - return self.manager.list_jobs() |
2506 | |
2507 | === removed file 'dmedia/gtk/tests/test_client.py' |
2508 | --- dmedia/gtk/tests/test_client.py 2011-09-06 19:38:32 +0000 |
2509 | +++ dmedia/gtk/tests/test_client.py 1970-01-01 00:00:00 +0000 |
2510 | @@ -1,284 +0,0 @@ |
2511 | -# Authors: |
2512 | -# Jason Gerard DeRose <jderose@novacut.com> |
2513 | -# |
2514 | -# dmedia: distributed media library |
2515 | -# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com> |
2516 | -# |
2517 | -# This file is part of `dmedia`. |
2518 | -# |
2519 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
2520 | -# terms of the GNU Affero General Public License as published by the Free |
2521 | -# Software Foundation, either version 3 of the License, or (at your option) any |
2522 | -# later version. |
2523 | -# |
2524 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
2525 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
2526 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
2527 | -# details. |
2528 | -# |
2529 | -# You should have received a copy of the GNU Affero General Public License along |
2530 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
2531 | - |
2532 | -""" |
2533 | -Unit tests for `dmedia.client` module. |
2534 | -""" |
2535 | - |
2536 | -import os |
2537 | -from os import path |
2538 | -from subprocess import Popen |
2539 | -import time |
2540 | -import json |
2541 | - |
2542 | -import dbus |
2543 | -from dbus.proxies import ProxyObject |
2544 | -from gi.repository import GObject |
2545 | - |
2546 | -import dmedia |
2547 | -from dmedia.constants import VIDEO, AUDIO, IMAGE, EXTENSIONS |
2548 | -from dmedia.gtkui import client, service |
2549 | - |
2550 | -from dmedia.tests.helpers import TempDir, random_bus, prep_import_source |
2551 | -from dmedia.tests.helpers import sample_mov, sample_thm |
2552 | -from dmedia.tests.helpers import mov_hash, thm_hash |
2553 | -from dmedia.tests.couch import CouchCase |
2554 | - |
2555 | - |
2556 | -tree = path.dirname(path.dirname(path.abspath(dmedia.__file__))) |
2557 | -assert path.isfile(path.join(tree, 'setup.py')) |
2558 | -script = path.join(tree, 'dmedia-importer-service') |
2559 | -assert path.isfile(script) |
2560 | - |
2561 | - |
2562 | -class CaptureCallback(object): |
2563 | - def __init__(self, signal, messages): |
2564 | - self.signal = signal |
2565 | - self.messages = messages |
2566 | - self.callback = None |
2567 | - |
2568 | - def __call__(self, *args): |
2569 | - self.messages.append( |
2570 | - (self.signal,) + args |
2571 | - ) |
2572 | - if callable(self.callback): |
2573 | - self.callback() |
2574 | - |
2575 | - |
2576 | -class SignalCapture(object): |
2577 | - def __init__(self, obj, *signals): |
2578 | - self.obj = obj |
2579 | - self.messages = [] |
2580 | - self.handlers = {} |
2581 | - for name in signals: |
2582 | - callback = CaptureCallback(name, self.messages) |
2583 | - obj.connect(name, callback) |
2584 | - self.handlers[name] = callback |
2585 | - |
2586 | - |
2587 | -class TestClient(CouchCase): |
2588 | - klass = client.Client |
2589 | - |
2590 | - def setUp(self): |
2591 | - """ |
2592 | - Launch dmedia dbus service using a random bus name. |
2593 | - |
2594 | - This will launch dmedia-service with a random bus name like this: |
2595 | - |
2596 | - dmedia-service --bus org.test3ISHAWZVSWVN5I5S.DMedia |
2597 | - |
2598 | - How do people usually unit test dbus services? This works, but not sure |
2599 | - if there is a better idiom in common use. --jderose |
2600 | - """ |
2601 | - self.skipTest('intermittent DBus failure') |
2602 | - super(TestClient, self).setUp() |
2603 | - self.bus = random_bus() |
2604 | - cmd = [script, '--no-gui', |
2605 | - '--bus', self.bus, |
2606 | - '--env', json.dumps(self.env), |
2607 | - ] |
2608 | - self.service = Popen(cmd) |
2609 | - time.sleep(1) # Give dmedia-service time to start |
2610 | - |
2611 | - def tearDown(self): |
2612 | - super(TestClient, self).tearDown() |
2613 | - try: |
2614 | - self.service.terminate() |
2615 | - self.service.wait() |
2616 | - except OSError: |
2617 | - pass |
2618 | - finally: |
2619 | - self.service = None |
2620 | - |
2621 | - def new(self): |
2622 | - return self.klass(bus=self.bus) |
2623 | - |
2624 | - def test_init(self): |
2625 | - # Test with no bus |
2626 | - inst = self.klass() |
2627 | - self.assertEqual(inst._bus, 'org.freedesktop.DMediaImporter') |
2628 | - self.assertTrue(isinstance(inst._conn, dbus.SessionBus)) |
2629 | - self.assertTrue(inst._proxy is None) |
2630 | - |
2631 | - # Test with random bus |
2632 | - inst = self.new() |
2633 | - self.assertEqual(inst._bus, self.bus) |
2634 | - self.assertTrue(isinstance(inst._conn, dbus.SessionBus)) |
2635 | - self.assertTrue(inst._proxy is None) |
2636 | - |
2637 | - # Test the proxy property |
2638 | - p = inst.proxy |
2639 | - self.assertTrue(isinstance(p, ProxyObject)) |
2640 | - self.assertTrue(p is inst._proxy) |
2641 | - self.assertTrue(p is inst.proxy) |
2642 | - |
2643 | - # Test version() |
2644 | - self.assertEqual(inst.version(), dmedia.__version__) |
2645 | - |
2646 | - # Test get_extensions() |
2647 | - self.assertEqual(inst.get_extensions(['video']), sorted(VIDEO)) |
2648 | - self.assertEqual(inst.get_extensions(['audio']), sorted(AUDIO)) |
2649 | - self.assertEqual(inst.get_extensions(['image']), sorted(IMAGE)) |
2650 | - self.assertEqual(inst.get_extensions(['all']), sorted(EXTENSIONS)) |
2651 | - self.assertEqual( |
2652 | - inst.get_extensions(['video', 'audio']), |
2653 | - sorted(VIDEO + AUDIO) |
2654 | - ) |
2655 | - self.assertEqual( |
2656 | - inst.get_extensions(['video', 'image']), |
2657 | - sorted(VIDEO + IMAGE) |
2658 | - ) |
2659 | - self.assertEqual( |
2660 | - inst.get_extensions(['audio', 'image']), |
2661 | - sorted(AUDIO + IMAGE) |
2662 | - ) |
2663 | - self.assertEqual( |
2664 | - inst.get_extensions(['video', 'audio', 'image']), |
2665 | - sorted(EXTENSIONS) |
2666 | - ) |
2667 | - self.assertEqual( |
2668 | - inst.get_extensions(['video', 'audio', 'image', 'all']), |
2669 | - sorted(EXTENSIONS) |
2670 | - ) |
2671 | - self.assertEqual(inst.get_extensions(['foo', 'bar']), []) |
2672 | - |
2673 | - def test_connect(self): |
2674 | - def callback(*args): |
2675 | - pass |
2676 | - |
2677 | - inst = self.new() |
2678 | - self.assertEqual(inst._proxy, None) |
2679 | - inst.connect('import_started', callback) |
2680 | - self.assertTrue(isinstance(inst._proxy, ProxyObject)) |
2681 | - self.assertTrue(inst._proxy is inst.proxy) |
2682 | - |
2683 | - def test_kill(self): |
2684 | - inst = self.new() |
2685 | - self.assertEqual(self.service.poll(), None) |
2686 | - inst.kill() |
2687 | - self.assertTrue(inst._proxy is None) |
2688 | - time.sleep(1) # Give dmedia-service time to shutdown |
2689 | - self.assertEqual(self.service.poll(), 0) |
2690 | - |
2691 | - def test_import(self): |
2692 | - inst = self.new() |
2693 | - signals = SignalCapture(inst, |
2694 | - 'batch_started', |
2695 | - 'import_started', |
2696 | - 'import_count', |
2697 | - 'import_progress', |
2698 | - 'import_finished', |
2699 | - 'batch_finished', |
2700 | - ) |
2701 | - mainloop = GObject.MainLoop() |
2702 | - signals.handlers['batch_finished'].callback = mainloop.quit |
2703 | - |
2704 | - tmp = TempDir() |
2705 | - base = tmp.path |
2706 | - |
2707 | - # Test with relative path |
2708 | - self.assertEqual( |
2709 | - inst.start_import('some/relative/path'), |
2710 | - 'not_abspath' |
2711 | - ) |
2712 | - self.assertEqual( |
2713 | - inst.start_import('/media/EOS_DIGITAL/../../etc/ssh'), |
2714 | - 'not_abspath' |
2715 | - ) |
2716 | - |
2717 | - # Test with non-dir |
2718 | - nope = tmp.join('memory_card') |
2719 | - self.assertEqual(inst.start_import(nope), 'not_a_dir') |
2720 | - nope = tmp.touch('memory_card') |
2721 | - self.assertEqual(inst.start_import(nope), 'not_a_dir') |
2722 | - os.unlink(nope) |
2723 | - |
2724 | - # Test a real import |
2725 | - (src1, src2, dup1) = prep_import_source(tmp) |
2726 | - mov_size = path.getsize(sample_mov) |
2727 | - thm_size = path.getsize(sample_thm) |
2728 | - self.assertEqual(inst.list_imports(), []) |
2729 | - self.assertEqual(inst.stop_import(base), 'not_running') |
2730 | - self.assertEqual(inst.start_import(base), 'started') |
2731 | - self.assertEqual(inst.start_import(base), 'already_running') |
2732 | - self.assertEqual(inst.list_imports(), [base]) |
2733 | - |
2734 | - # mainloop.quit() gets called at 'batch_finished' signal |
2735 | - mainloop.run() |
2736 | - |
2737 | - self.assertEqual(inst.list_imports(), []) |
2738 | - self.assertEqual(inst.stop_import(base), 'not_running') |
2739 | - |
2740 | - self.assertEqual(len(signals.messages), 8) |
2741 | - batch_id = signals.messages[0][2] |
2742 | - self.assertEqual( |
2743 | - signals.messages[0], |
2744 | - ('batch_started', inst, batch_id) |
2745 | - ) |
2746 | - import_id = signals.messages[1][3] |
2747 | - self.assertEqual( |
2748 | - signals.messages[1], |
2749 | - ('import_started', inst, base, import_id) |
2750 | - ) |
2751 | - self.assertEqual( |
2752 | - signals.messages[2], |
2753 | - ('import_count', inst, base, import_id, 3) |
2754 | - ) |
2755 | - self.assertEqual( |
2756 | - signals.messages[3], |
2757 | - ('import_progress', inst, base, import_id, 1, 3, |
2758 | - dict(action='imported', src=src1) |
2759 | - ) |
2760 | - ) |
2761 | - self.assertEqual( |
2762 | - signals.messages[4], |
2763 | - ('import_progress', inst, base, import_id, 2, 3, |
2764 | - dict(action='imported', src=src2) |
2765 | - ) |
2766 | - ) |
2767 | - self.assertEqual( |
2768 | - signals.messages[5], |
2769 | - ('import_progress', inst, base, import_id, 3, 3, |
2770 | - dict(action='skipped', src=dup1) |
2771 | - ) |
2772 | - ) |
2773 | - self.assertEqual( |
2774 | - signals.messages[6], |
2775 | - ('import_finished', inst, base, import_id, |
2776 | - dict( |
2777 | - imported=2, |
2778 | - imported_bytes=(mov_size + thm_size), |
2779 | - skipped=1, |
2780 | - skipped_bytes=mov_size, |
2781 | - ) |
2782 | - ) |
2783 | - ) |
2784 | - self.assertEqual( |
2785 | - signals.messages[7], |
2786 | - ('batch_finished', inst, batch_id, |
2787 | - dict( |
2788 | - imported=2, |
2789 | - imported_bytes=(mov_size + thm_size), |
2790 | - skipped=1, |
2791 | - skipped_bytes=mov_size, |
2792 | - ) |
2793 | - ) |
2794 | - ) |
2795 | |
2796 | === removed file 'dmedia/gtk/tests/test_firstrun.py' |
2797 | --- dmedia/gtk/tests/test_firstrun.py 2011-03-27 08:01:30 +0000 |
2798 | +++ dmedia/gtk/tests/test_firstrun.py 1970-01-01 00:00:00 +0000 |
2799 | @@ -1,35 +0,0 @@ |
2800 | -# Authors: |
2801 | -# Jason Gerard DeRose <jderose@novacut.com> |
2802 | -# |
2803 | -# dmedia: distributed media library |
2804 | -# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com> |
2805 | -# |
2806 | -# This file is part of `dmedia`. |
2807 | -# |
2808 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
2809 | -# terms of the GNU Affero General Public License as published by the Free |
2810 | -# Software Foundation, either version 3 of the License, or (at your option) any |
2811 | -# later version. |
2812 | -# |
2813 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
2814 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
2815 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
2816 | -# details. |
2817 | -# |
2818 | -# You should have received a copy of the GNU Affero General Public License along |
2819 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
2820 | - |
2821 | -""" |
2822 | -Unit tests for `dmedia.firstrun` module. |
2823 | -""" |
2824 | - |
2825 | -from unittest import TestCase |
2826 | - |
2827 | -from dmedia.gtkui import firstrun |
2828 | - |
2829 | - |
2830 | -class test_FirstRunGUI(TestCase): |
2831 | - klass = firstrun.FirstRunGUI |
2832 | - |
2833 | - def test_init(self): |
2834 | - inst = self.klass() |
2835 | |
2836 | === removed file 'dmedia/gtk/tests/test_widgets.py' |
2837 | --- dmedia/gtk/tests/test_widgets.py 2011-08-31 18:17:37 +0000 |
2838 | +++ dmedia/gtk/tests/test_widgets.py 1970-01-01 00:00:00 +0000 |
2839 | @@ -1,142 +0,0 @@ |
2840 | -# Authors: |
2841 | -# Jason Gerard DeRose <jderose@novacut.com> |
2842 | -# |
2843 | -# dmedia: distributed media library |
2844 | -# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com> |
2845 | -# |
2846 | -# This file is part of `dmedia`. |
2847 | -# |
2848 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
2849 | -# terms of the GNU Affero General Public License as published by the Free |
2850 | -# Software Foundation, either version 3 of the License, or (at your option) any |
2851 | -# later version. |
2852 | -# |
2853 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
2854 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
2855 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
2856 | -# details. |
2857 | -# |
2858 | -# You should have received a copy of the GNU Affero General Public License along |
2859 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
2860 | - |
2861 | -""" |
2862 | -Unit tests for `dmedia.gtkui.widgets` module. |
2863 | -""" |
2864 | - |
2865 | -from unittest import TestCase |
2866 | - |
2867 | - |
2868 | -from dmedia.schema import random_id |
2869 | -from dmedia.gtkui import widgets |
2870 | - |
2871 | - |
2872 | -# Test oauth tokens - don't relpace with tokens you actually use! |
2873 | -# Also, don't actually use these! |
2874 | -tokens = { |
2875 | - 'consumer_key': 'cVMqVGDNkC', |
2876 | - 'consumer_secret': 'lhpRuaFaeI', |
2877 | - 'token': 'NBGPaRrdXK', |
2878 | - 'token_secret': 'SGtppOobin' |
2879 | -} |
2880 | - |
2881 | - |
2882 | -class DummyRequest(object): |
2883 | - def __init__(self, uri): |
2884 | - self._uri = uri |
2885 | - |
2886 | - def get_uri(self): |
2887 | - return self._uri |
2888 | - |
2889 | - |
2890 | -class DummyPolicy(object): |
2891 | - def __init__(self): |
2892 | - self._calls = [] |
2893 | - |
2894 | - def ignore(self): |
2895 | - self._calls.append('ignore') |
2896 | - |
2897 | - def use(self): |
2898 | - self._calls.append('use') |
2899 | - |
2900 | - def download(self): |
2901 | - self._calls.append('download') |
2902 | - |
2903 | - |
2904 | -class SignalCollector(object): |
2905 | - def __init__(self, couchview): |
2906 | - self._couchview = couchview |
2907 | - couchview.connect('play', self.on_play) |
2908 | - couchview.connect('open', self.on_open) |
2909 | - self._sigs = [] |
2910 | - |
2911 | - def on_play(self, cv, *args): |
2912 | - assert cv is self._couchview |
2913 | - self._sigs.append(('play',) + args) |
2914 | - |
2915 | - def on_open(self, cv, *args): |
2916 | - assert cv is self._couchview |
2917 | - self._sigs.append(('open',) + args) |
2918 | - |
2919 | - |
2920 | -class TestCouchView(TestCase): |
2921 | - klass = widgets.CouchView |
2922 | - |
2923 | - def test_init(self): |
2924 | - url = 'http://localhost:40705/dmedia/' |
2925 | - netloc = 'localhost:40705' |
2926 | - |
2927 | - # Test with no oauth tokens provided: |
2928 | - inst = self.klass(url) |
2929 | - self.assertEqual(inst._couch_url, url) |
2930 | - self.assertEqual(inst._couch_netloc, netloc) |
2931 | - self.assertIsNone(inst._oauth) |
2932 | - |
2933 | - # Test with oauth tokens: |
2934 | - inst = self.klass(url, oauth_tokens=tokens) |
2935 | - self.assertEqual(inst._couch_url, url) |
2936 | - self.assertEqual(inst._couch_netloc, netloc) |
2937 | - self.assertIs(inst._oauth, tokens) |
2938 | - |
2939 | - def test_on_nav_policy_decision(self): |
2940 | - # Method signature: |
2941 | - # CouchView_on_nav_policy_decision(view, frame, request, nav, policy) |
2942 | - |
2943 | - url = 'http://localhost:40705/dmedia/' |
2944 | - inst = self.klass(url) |
2945 | - s = SignalCollector(inst) |
2946 | - p = DummyPolicy() |
2947 | - |
2948 | - # Test a requset to desktopcouch |
2949 | - r = DummyRequest('http://localhost:40705/foo/bar/baz') |
2950 | - self.assertFalse( |
2951 | - inst._on_nav_policy_decision(None, None, r, None, p) |
2952 | - ) |
2953 | - self.assertEqual(p._calls, []) |
2954 | - self.assertEqual(s._sigs, []) |
2955 | - |
2956 | - # Test a play:foo URI |
2957 | - play = 'play:' + random_id() + '?start=17&end=69' |
2958 | - r = DummyRequest(play) |
2959 | - self.assertTrue( |
2960 | - inst._on_nav_policy_decision(None, None, r, None, p) |
2961 | - ) |
2962 | - self.assertEqual(p._calls, ['ignore']) |
2963 | - self.assertEqual(s._sigs, [('play', play)]) |
2964 | - |
2965 | - # Test opening an external URL |
2966 | - lp = 'https://launchpad.net/dmedia' |
2967 | - r = DummyRequest(lp) |
2968 | - self.assertTrue( |
2969 | - inst._on_nav_policy_decision(None, None, r, None, p) |
2970 | - ) |
2971 | - self.assertEqual(p._calls, ['ignore', 'ignore']) |
2972 | - self.assertEqual(s._sigs, [('play', play), ('open', lp)]) |
2973 | - |
2974 | - # Test a URI that will just be ignored, not emit a signal |
2975 | - nope = 'ftp://example.com' |
2976 | - r = DummyRequest(nope) |
2977 | - self.assertTrue( |
2978 | - inst._on_nav_policy_decision(None, None, r, None, p) |
2979 | - ) |
2980 | - self.assertEqual(p._calls, ['ignore', 'ignore', 'ignore']) |
2981 | - self.assertEqual(s._sigs, [('play', play), ('open', lp)]) |
2982 | |
2983 | === removed file 'dmedia/gtk/widgets.py' |
2984 | --- dmedia/gtk/widgets.py 2011-08-31 18:17:37 +0000 |
2985 | +++ dmedia/gtk/widgets.py 1970-01-01 00:00:00 +0000 |
2986 | @@ -1,225 +0,0 @@ |
2987 | -# Authors: |
2988 | -# Jason Gerard DeRose <jderose@novacut.com> |
2989 | -# David Green <david4dev@gmail.com> |
2990 | -# |
2991 | -# dmedia: distributed media library |
2992 | -# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com> |
2993 | -# |
2994 | -# This file is part of `dmedia`. |
2995 | -# |
2996 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
2997 | -# terms of the GNU Affero General Public License as published by the Free |
2998 | -# Software Foundation, either version 3 of the License, or (at your option) any |
2999 | -# later version. |
3000 | -# |
3001 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
3002 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
3003 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
3004 | -# details. |
3005 | -# |
3006 | -# You should have received a copy of the GNU Affero General Public License along |
3007 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
3008 | - |
3009 | -""" |
3010 | -Custom dmedia GTK widgets, currently just `CouchView` and `BrowserMenu`. |
3011 | -""" |
3012 | - |
3013 | -from urlparse import urlparse, parse_qsl |
3014 | - |
3015 | -from microfiber import _oauth_header |
3016 | -from gi.repository import GObject, WebKit, Gtk |
3017 | - |
3018 | -from .menu import MENU, ACTIONS |
3019 | -from gettext import gettext as _ |
3020 | - |
3021 | - |
3022 | -class CouchView(WebKit.WebView): |
3023 | - """ |
3024 | - Transparently sign desktopcouch requests with OAuth. |
3025 | - |
3026 | - desktopcouch uses OAuth to authenticate HTTP requests to CouchDB. Well, |
3027 | - technically it can also use basic auth, but if you do this, Stuart Langridge |
3028 | - will be very cross with you! |
3029 | - |
3030 | - This class wraps a ``gi.repository.WebKit.WebView`` so that you can have a |
3031 | - single web app that: |
3032 | - |
3033 | - 1. Can run in a browser and talk to a remote CouchDB over HTTPS with |
3034 | - basic auth |
3035 | - |
3036 | - 2. Can also run in embedded WebKit and talk to the local desktopcouch |
3037 | - over HTTP with OAuth |
3038 | - |
3039 | - Being able to do this sort of thing transparently is a big reason why dmedia |
3040 | - and Novacut are designed the way they are. |
3041 | - |
3042 | - For some background, see: |
3043 | - |
3044 | - https://bugs.launchpad.net/dmedia/+bug/677697 |
3045 | - |
3046 | - http://oauth.net/ |
3047 | - |
3048 | - Special thanks to Stuart Langridge for the example code that helped get this |
3049 | - working. |
3050 | - """ |
3051 | - |
3052 | - __gsignals__ = { |
3053 | - 'play': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
3054 | - [GObject.TYPE_PYOBJECT] |
3055 | - ), |
3056 | - 'open': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
3057 | - [GObject.TYPE_PYOBJECT] |
3058 | - ), |
3059 | - } |
3060 | - |
3061 | - def __init__(self, couch_url, oauth_tokens=None): |
3062 | - super(CouchView, self).__init__() |
3063 | - self._couch_url = couch_url |
3064 | - self._couch_netloc = urlparse(couch_url).netloc |
3065 | - self.connect('resource-request-starting', self._on_resource_request) |
3066 | - self.connect('navigation-policy-decision-requested', |
3067 | - self._on_nav_policy_decision |
3068 | - ) |
3069 | - self._oauth = oauth_tokens |
3070 | - |
3071 | - def _on_nav_policy_decision(self, view, frame, request, nav, policy): |
3072 | - """ |
3073 | - Handle user trying to Navigate away from current page. |
3074 | - |
3075 | - Note that this will be called before `CouchView._on_resource_request()`. |
3076 | - |
3077 | - The *policy* arg is a ``WebPolicyDecision`` instance. To handle the |
3078 | - decision, call one of: |
3079 | - |
3080 | - * ``WebPolicyDecision.ignore()`` |
3081 | - * ``WebPolicyDecision.use()`` |
3082 | - * ``WebPolicyDecision.download()`` |
3083 | - |
3084 | - And then return ``True``. |
3085 | - |
3086 | - Otherwise, return ``False`` or ``None`` to have the WebKit default |
3087 | - behavior apply. |
3088 | - """ |
3089 | - uri = request.get_uri() |
3090 | - u = urlparse(uri) |
3091 | - if u.netloc == self._couch_netloc and u.scheme in ('http', 'https'): |
3092 | - return False |
3093 | - if uri.startswith('play:'): |
3094 | - self.emit('play', uri) |
3095 | - elif u.netloc != self._couch_netloc and u.scheme in ('http', 'https'): |
3096 | - self.emit('open', uri) |
3097 | - policy.ignore() |
3098 | - return True |
3099 | - |
3100 | - def _on_resource_request(self, view, frame, resource, request, response): |
3101 | - """ |
3102 | - When appropriate, OAuth-sign the request prior to it being sent. |
3103 | - |
3104 | - This will only sign requests on the same host and port in the URL passed |
3105 | - to `CouchView.__init__()`. |
3106 | - """ |
3107 | - uri = request.get_uri() |
3108 | - u = urlparse(uri) |
3109 | - if u.scheme not in ('http', 'https'): # Ignore data:foo requests |
3110 | - return |
3111 | - if u.netloc != self._couch_netloc: # Dont sign requests to broader net! |
3112 | - return |
3113 | - if not self._oauth: # OAuth info wasn't provided |
3114 | - return |
3115 | - query = dict(parse_qsl(u.query)) |
3116 | - # Handle bloody CouchDB having foo.html?dbname URLs, that is |
3117 | - # a querystring which isn't of the form foo=bar |
3118 | - if u.query and not query: |
3119 | - query = {u.query: ''} |
3120 | - baseurl = ''.join([u.scheme, '://', u.netloc, u.path]) |
3121 | - method = request.props.message.props.method |
3122 | - h = _oauth_header(self._oauth, method, baseurl, query) |
3123 | - for key in h: |
3124 | - request.props.message.props.request_headers.append(k, h[k]) |
3125 | - |
3126 | - |
3127 | -class BrowserMenu(Gtk.MenuBar): |
3128 | - """ |
3129 | - The BrowserMenu class creates a menubar for dmedia-gtk, the dmedia |
3130 | - media browser. |
3131 | - |
3132 | - The menu argument specifies the layout of the menu as a list of menubar |
3133 | - items. Each item is a dictionary. There are 2 main types of item: action and |
3134 | - menu. |
3135 | - |
3136 | - Actions are menu items that do something when clicked. The dictionary |
3137 | - for an action looks like this: |
3138 | - { |
3139 | - "label" : "text to display", |
3140 | - "type" : "action", |
3141 | - "action" : "action id" |
3142 | - } |
3143 | - The label is the text to display (eg. "Close"). The type tells BrowserMenu |
3144 | - that this item is an action not a menu. The action is a string that is looked |
3145 | - up in the actions dictionary and refers to a callback function that is executed |
3146 | - when this menu item is clicked. |
3147 | - |
3148 | - Menus are submenus of the menubar. These can hold other menus and actions. |
3149 | - The dictionary for a menu looks like this: |
3150 | - { |
3151 | - "label" : "text to display", |
3152 | - "type" : "menu", |
3153 | - "items" : [item_1, item_2 ... item_n] |
3154 | - } |
3155 | - The label is the text to display (eg. "File"). The type tells BrowserMenu |
3156 | - that this item is a menu not an action. "items" is a list of other items |
3157 | - that appear in this menu. |
3158 | - |
3159 | - The actions argument is a dictionary of action IDs (strings) and callback |
3160 | - functions. |
3161 | - { |
3162 | - "action1" : lambda *a: ... , |
3163 | - "action2" : my_object.method, |
3164 | - "action3" : some_function |
3165 | - } |
3166 | - |
3167 | - If menu or actions are not specified the default will be MENU and |
3168 | - ACTIONS repectively which are defined in menu.py. |
3169 | - |
3170 | - In addition to the main 2 types of menu item, there is a "custom" |
3171 | - item that allows for any gtk widget to be put in the menu as long |
3172 | - as gtk itself allows for this. |
3173 | - |
3174 | - The dictionary for a custom item looks like this: |
3175 | - { |
3176 | - "type" : "custom", |
3177 | - "widget" : gtk_widget |
3178 | - } |
3179 | - """ |
3180 | - def __init__(self, menu=MENU, actions=ACTIONS): |
3181 | - super(BrowserMenu, self).__init__() |
3182 | - self.show() |
3183 | - self.menu = menu |
3184 | - self.actions = actions |
3185 | - self.add_items_to_menu(self, *self.make_menu(self.menu)) |
3186 | - |
3187 | - def add_items_to_menu(self, menu, *items): |
3188 | - for item in items: |
3189 | - menu.append(item) |
3190 | - |
3191 | - def make_menu(self, menu): |
3192 | - items = [] |
3193 | - for i in menu: |
3194 | - if i["type"] == "custom": |
3195 | - items.append(i["widget"]) #allows for custom widgets, eg. separators |
3196 | - else: |
3197 | - item = Gtk.MenuItem() |
3198 | - item.show() |
3199 | - item.set_property("use-underline", True) #allow keyboard nav |
3200 | - item.set_label(_(i["label"])) |
3201 | - if i["type"] == "menu": |
3202 | - submenu = Gtk.Menu() |
3203 | - submenu.show() |
3204 | - self.add_items_to_menu(submenu, *self.make_menu(i["items"])) |
3205 | - item.set_submenu(submenu) |
3206 | - elif i["type"] == "action": |
3207 | - item.connect("activate", self.actions[i["action"]]) |
3208 | - items.append(item) |
3209 | - return items |
3210 | - |
3211 | - |
3212 | |
3213 | === removed file 'dmedia/service/tests/test_api.py' |
3214 | --- dmedia/service/tests/test_api.py 2011-09-16 03:35:25 +0000 |
3215 | +++ dmedia/service/tests/test_api.py 1970-01-01 00:00:00 +0000 |
3216 | @@ -1,110 +0,0 @@ |
3217 | -# Authors: |
3218 | -# Jason Gerard DeRose <jderose@novacut.com> |
3219 | -# |
3220 | -# dmedia: distributed media library |
3221 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
3222 | -# |
3223 | -# This file is part of `dmedia`. |
3224 | -# |
3225 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
3226 | -# terms of the GNU Affero General Public License as published by the Free |
3227 | -# Software Foundation, either version 3 of the License, or (at your option) any |
3228 | -# later version. |
3229 | -# |
3230 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
3231 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
3232 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
3233 | -# details. |
3234 | -# |
3235 | -# You should have received a copy of the GNU Affero General Public License along |
3236 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
3237 | - |
3238 | -""" |
3239 | -Unit tests for `dmedia.api` module. |
3240 | -""" |
3241 | - |
3242 | -import os |
3243 | -from os import path |
3244 | -from subprocess import Popen |
3245 | -import json |
3246 | -import time |
3247 | - |
3248 | -from microfiber import NotFound |
3249 | - |
3250 | -import dmedia |
3251 | -from dmedia.abstractcouch import get_db |
3252 | -from dmedia import api |
3253 | - |
3254 | -from .couch import CouchCase |
3255 | -from .helpers import random_bus |
3256 | - |
3257 | - |
3258 | -tree = path.dirname(path.dirname(path.abspath(dmedia.__file__))) |
3259 | -assert path.isfile(path.join(tree, 'setup.py')) |
3260 | -script = path.join(tree, 'dmedia-service') |
3261 | -assert path.isfile(script) |
3262 | - |
3263 | - |
3264 | -class TestDMedia(CouchCase): |
3265 | - klass = api.DMedia |
3266 | - |
3267 | - def setUp(self): |
3268 | - """ |
3269 | - Launch dmedia dbus service using a random bus name. |
3270 | - |
3271 | - This will launch dmedia-service with a random bus name like this: |
3272 | - |
3273 | - dmedia-service --bus org.test3ISHAWZVSWVN5I5S.DMedia |
3274 | - |
3275 | - How do people usually unit test dbus services? This works, but not sure |
3276 | - if there is a better idiom in common use. --jderose |
3277 | - """ |
3278 | - super(TestDMedia, self).setUp() |
3279 | - self.bus = random_bus() |
3280 | - cmd = [script, |
3281 | - '--bus', self.bus, |
3282 | - '--env', json.dumps(self.env), |
3283 | - ] |
3284 | - self.service = Popen(cmd) |
3285 | - time.sleep(1) # Give dmedia-service time to start |
3286 | - |
3287 | - def tearDown(self): |
3288 | - super(TestDMedia, self).tearDown() |
3289 | - try: |
3290 | - self.service.terminate() |
3291 | - self.service.wait() |
3292 | - except OSError: |
3293 | - pass |
3294 | - finally: |
3295 | - self.service = None |
3296 | - |
3297 | - def test_all(self): |
3298 | - inst = self.klass(self.bus) |
3299 | - |
3300 | - # DMedia.Version() |
3301 | - self.assertEqual(inst.version(), dmedia.__version__) |
3302 | - |
3303 | - # DMedia.GetEnv() |
3304 | - env = inst.get_env() |
3305 | - self.assertEqual(env['oauth'], self.env['oauth']) |
3306 | - self.assertEqual(env['url'], self.env['url']) |
3307 | - self.assertEqual(env['dbname'], self.env['dbname']) |
3308 | - |
3309 | - # DMedia.HasApp() |
3310 | - db = get_db(self.env) |
3311 | - with self.assertRaises(NotFound) as cm: |
3312 | - db.get('app') |
3313 | - self.assertTrue(inst.has_app()) |
3314 | - self.assertTrue(db.get('app')['_rev'].startswith('1-')) |
3315 | - self.assertTrue(inst.has_app()) |
3316 | - self.assertTrue(db.get('app')['_rev'].startswith('1-')) |
3317 | - |
3318 | - # DMedia.ListTransfers() |
3319 | - self.assertEqual(inst.list_transfers(), []) |
3320 | - |
3321 | - # DMedia.Kill() |
3322 | - self.assertIsNone(self.service.poll(), None) |
3323 | - inst.kill() |
3324 | - self.assertTrue(inst._proxy is None) |
3325 | - time.sleep(1) # Give dmedia-service time to shutdown |
3326 | - self.assertEqual(self.service.poll(), 0) |
3327 | |
3328 | === added file 'dmedia/service/tests/test_udisks.py' |
3329 | --- dmedia/service/tests/test_udisks.py 1970-01-01 00:00:00 +0000 |
3330 | +++ dmedia/service/tests/test_udisks.py 2012-04-05 13:22:39 +0000 |
3331 | @@ -0,0 +1,183 @@ |
3332 | +# dmedia: dmedia hashing protocol and file layout |
3333 | +# Copyright (C) 2012 Novacut Inc |
3334 | +# |
3335 | +# This file is part of `dmedia`. |
3336 | +# |
3337 | +# `dmedia` is free software: you can redistribute it and/or modify it under |
3338 | +# the terms of the GNU Affero General Public License as published by the Free |
3339 | +# Software Foundation, either version 3 of the License, or (at your option) any |
3340 | +# later version. |
3341 | +# |
3342 | +# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
3343 | +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
3344 | +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
3345 | +# details. |
3346 | +# |
3347 | +# You should have received a copy of the GNU Affero General Public License along |
3348 | +# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
3349 | +# |
3350 | +# Authors: |
3351 | +# Jason Gerard DeRose <jderose@novacut.com> |
3352 | + |
3353 | +""" |
3354 | +Unit tests for `dmedia.service.udisks`. |
3355 | +""" |
3356 | + |
3357 | +from unittest import TestCase |
3358 | +import os |
3359 | +from os import path |
3360 | +import json |
3361 | + |
3362 | +from microfiber import random_id |
3363 | + |
3364 | +from dmedia.tests.base import TempDir |
3365 | +from dmedia.service import udisks |
3366 | + |
3367 | + |
3368 | +class TestFunctions(TestCase): |
3369 | + def test_major_minor(self): |
3370 | + tmp = TempDir() |
3371 | + st_dev = os.stat(tmp.dir).st_dev |
3372 | + major = os.major(st_dev) |
3373 | + minor = os.minor(st_dev) |
3374 | + |
3375 | + # Test on the tmp dir: |
3376 | + self.assertEqual(udisks.major_minor(tmp.dir), (major, minor)) |
3377 | + |
3378 | + # Test on a subdir |
3379 | + subdir = tmp.makedirs('subdir') |
3380 | + self.assertEqual(udisks.major_minor(subdir), (major, minor)) |
3381 | + |
3382 | + # Test on a file |
3383 | + some_file = tmp.touch('some_file') |
3384 | + self.assertEqual(udisks.major_minor(some_file), (major, minor)) |
3385 | + |
3386 | + # Test on a non-existant path |
3387 | + nope = tmp.join('nope') |
3388 | + with self.assertRaises(OSError) as cm: |
3389 | + udisks.major_minor(nope) |
3390 | + self.assertEqual(cm.exception.errno, 2) |
3391 | + |
3392 | + def test_usable_mount(self): |
3393 | + self.assertEqual(udisks.usable_mount(['/media/foo']), '/media/foo') |
3394 | + self.assertEqual(udisks.usable_mount(['/srv/bar']), '/srv/bar') |
3395 | + |
3396 | + self.assertIsNone(udisks.usable_mount(['/media'])) |
3397 | + self.assertIsNone(udisks.usable_mount(['/srv'])) |
3398 | + self.assertIsNone(udisks.usable_mount(['/srv', '/media'])) |
3399 | + |
3400 | + self.assertEqual( |
3401 | + udisks.usable_mount(['/media/foo', '/srv/bar']), |
3402 | + '/media/foo' |
3403 | + ) |
3404 | + self.assertEqual( |
3405 | + udisks.usable_mount(['/srv/bar', '/media/foo']), |
3406 | + '/srv/bar' |
3407 | + ) |
3408 | + self.assertEqual( |
3409 | + udisks.usable_mount(['/', '/home', '/home/user', '/tmp', '/media/foo']), |
3410 | + '/media/foo' |
3411 | + ) |
3412 | + |
3413 | + def test_partition_info(self): |
3414 | + d = { |
3415 | + 'DeviceSize': 16130244608, |
3416 | + 'IdType': 'vfat', |
3417 | + 'IdVersion': 'FAT32', |
3418 | + 'IdLabel': 'H4N_SD', |
3419 | + 'PartitionNumber': 1, |
3420 | + 'IdUuid': '89E3-CE4D', |
3421 | + } |
3422 | + self.assertEqual(udisks.partition_info(d), |
3423 | + { |
3424 | + 'mount': None, |
3425 | + 'info': { |
3426 | + 'bytes': 16130244608, |
3427 | + 'filesystem': 'vfat', |
3428 | + 'filesystem_version': 'FAT32', |
3429 | + 'label': 'H4N_SD', |
3430 | + 'number': 1, |
3431 | + 'size': '16.1 GB', |
3432 | + 'uuid': '89E3-CE4D' |
3433 | + }, |
3434 | + } |
3435 | + ) |
3436 | + self.assertEqual(udisks.partition_info(d, '/media/foo'), |
3437 | + { |
3438 | + 'mount': '/media/foo', |
3439 | + 'info': { |
3440 | + 'bytes': 16130244608, |
3441 | + 'filesystem': 'vfat', |
3442 | + 'filesystem_version': 'FAT32', |
3443 | + 'label': 'H4N_SD', |
3444 | + 'number': 1, |
3445 | + 'size': '16.1 GB', |
3446 | + 'uuid': '89E3-CE4D' |
3447 | + }, |
3448 | + } |
3449 | + ) |
3450 | + |
3451 | + def test_drive_info(self): |
3452 | + d = { |
3453 | + 'DeviceSize': 16134438912, |
3454 | + 'DriveConnectionInterface': 'usb', |
3455 | + 'DriveModel': 'CFUDMASD', |
3456 | + 'DeviceIsSystemInternal': False, |
3457 | + 'DriveSerial': 'AA0000000009019', |
3458 | + } |
3459 | + self.assertEqual(udisks.drive_info(d), |
3460 | + { |
3461 | + 'partitions': {}, |
3462 | + 'info': { |
3463 | + 'bytes': 16134438912, |
3464 | + 'connection': 'usb', |
3465 | + 'model': 'CFUDMASD', |
3466 | + 'removable': True, |
3467 | + 'serial': 'AA0000000009019', |
3468 | + 'size': '16.1 GB', |
3469 | + 'text': '16.1 GB Removable Drive', |
3470 | + }, |
3471 | + } |
3472 | + ) |
3473 | + |
3474 | + d['DeviceIsSystemInternal'] = True |
3475 | + self.assertEqual(udisks.drive_info(d), |
3476 | + { |
3477 | + 'partitions': {}, |
3478 | + 'info': { |
3479 | + 'bytes': 16134438912, |
3480 | + 'connection': 'usb', |
3481 | + 'model': 'CFUDMASD', |
3482 | + 'removable': False, |
3483 | + 'serial': 'AA0000000009019', |
3484 | + 'size': '16.1 GB', |
3485 | + 'text': '16.1 GB Drive', |
3486 | + }, |
3487 | + } |
3488 | + ) |
3489 | + |
3490 | + def test_get_filestore_id(self): |
3491 | + tmp = TempDir() |
3492 | + |
3493 | + # Test when file and directory are missing |
3494 | + self.assertIsNone(udisks.get_filestore_id(tmp.dir)) |
3495 | + |
3496 | + # Test when control dir exists, file missing |
3497 | + basedir = tmp.makedirs('.dmedia') |
3498 | + self.assertTrue(path.isdir(basedir)) |
3499 | + self.assertIsNone(udisks.get_filestore_id(tmp.dir)) |
3500 | + |
3501 | + # Test when file is empty |
3502 | + store = tmp.touch('.dmedia', 'store.json') |
3503 | + self.assertTrue(path.isfile(store)) |
3504 | + self.assertIsNone(udisks.get_filestore_id(tmp.dir)) |
3505 | + |
3506 | + # Test when correct |
3507 | + _id = random_id() |
3508 | + json.dump({'_id': _id}, open(store, 'w')) |
3509 | + self.assertEqual(udisks.get_filestore_id(tmp.dir), _id) |
3510 | + |
3511 | + # Test when '_id' is missed from dict: |
3512 | + json.dump({'id': _id}, open(store, 'w')) |
3513 | + self.assertIsNone(udisks.get_filestore_id(tmp.dir)) |
3514 | + |
3515 | |
3516 | === added file 'dmedia/service/udisks.py' |
3517 | --- dmedia/service/udisks.py 1970-01-01 00:00:00 +0000 |
3518 | +++ dmedia/service/udisks.py 2012-04-05 13:22:39 +0000 |
3519 | @@ -0,0 +1,299 @@ |
3520 | +# dmedia: distributed media library |
3521 | +# Copyright (C) 2012 Novacut Inc |
3522 | +# |
3523 | +# This file is part of `dmedia`. |
3524 | +# |
3525 | +# `dmedia` is free software: you can redistribute it and/or modify it under |
3526 | +# the terms of the GNU Affero General Public License as published by the Free |
3527 | +# Software Foundation, either version 3 of the License, or (at your option) any |
3528 | +# later version. |
3529 | +# |
3530 | +# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
3531 | +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
3532 | +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
3533 | +# details. |
3534 | +# |
3535 | +# You should have received a copy of the GNU Affero General Public License along |
3536 | +# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
3537 | +# |
3538 | +# Authors: |
3539 | +# Jason Gerard DeRose <jderose@novacut.com> |
3540 | + |
3541 | +""" |
3542 | +Attempts to tame the UDisks beast. |
3543 | +""" |
3544 | + |
3545 | +import json |
3546 | +import os |
3547 | +from os import path |
3548 | +from gettext import gettext as _ |
3549 | +import logging |
3550 | + |
3551 | +from gi.repository import GObject |
3552 | +from filestore import DOTNAME |
3553 | + |
3554 | +from dmedia.units import bytes10 |
3555 | +from dmedia.service.dbus import system |
3556 | + |
3557 | + |
3558 | +log = logging.getLogger() |
3559 | +TYPE_PYOBJECT = GObject.TYPE_PYOBJECT |
3560 | + |
3561 | + |
3562 | +def major_minor(parentdir): |
3563 | + st_dev = os.stat(parentdir).st_dev |
3564 | + return (os.major(st_dev), os.minor(st_dev)) |
3565 | + |
3566 | + |
3567 | +def usable_mount(mounts): |
3568 | + """ |
3569 | + Return mount point at which Dmedia will look for file-stores. |
3570 | + |
3571 | + For example: |
3572 | + |
3573 | + >>> usable_mount(['/', '/tmp']) is None |
3574 | + True |
3575 | + >>> usable_mount(['/', '/tmp', '/media/foo']) |
3576 | + '/media/foo' |
3577 | + |
3578 | + """ |
3579 | + for mount in mounts: |
3580 | + if mount.startswith('/media/') or mount.startswith('/srv/'): |
3581 | + return mount |
3582 | + |
3583 | + |
3584 | +def partition_info(d, mount=None): |
3585 | + return { |
3586 | + 'mount': mount, |
3587 | + 'info': { |
3588 | + 'label': d['IdLabel'], |
3589 | + 'uuid': d['IdUuid'], |
3590 | + 'bytes': d['DeviceSize'], |
3591 | + 'size': bytes10(d['DeviceSize']), |
3592 | + 'filesystem': d['IdType'], |
3593 | + 'filesystem_version': d['IdVersion'], |
3594 | + 'number': d['PartitionNumber'], |
3595 | + }, |
3596 | + } |
3597 | + |
3598 | + |
3599 | +def drive_text(d): |
3600 | + if d['DeviceIsSystemInternal']: |
3601 | + template = _('{size} Drive') |
3602 | + else: |
3603 | + template = _('{size} Removable Drive') |
3604 | + return template.format(size=bytes10(d['DeviceSize'])) |
3605 | + |
3606 | + |
3607 | +def drive_info(d): |
3608 | + return { |
3609 | + 'partitions': {}, |
3610 | + 'info': { |
3611 | + 'serial': d['DriveSerial'], |
3612 | + 'bytes': d['DeviceSize'], |
3613 | + 'size': bytes10(d['DeviceSize']), |
3614 | + 'model': d['DriveModel'], |
3615 | + 'removable': not d['DeviceIsSystemInternal'], |
3616 | + 'connection': d['DriveConnectionInterface'], |
3617 | + 'text': drive_text(d), |
3618 | + } |
3619 | + } |
3620 | + |
3621 | + |
3622 | +def get_filestore_id(parentdir): |
3623 | + store = path.join(parentdir, DOTNAME, 'store.json') |
3624 | + try: |
3625 | + return json.load(open(store, 'r'))['_id'] |
3626 | + except Exception: |
3627 | + pass |
3628 | + |
3629 | + |
3630 | + |
3631 | +class Device: |
3632 | + __slots__ = ('obj', 'proxy', 'cache', 'ispartition', 'drive') |
3633 | + |
3634 | + def __init__(self, obj): |
3635 | + self.obj = obj |
3636 | + self.proxy = system.get( |
3637 | + 'org.freedesktop.UDisks', |
3638 | + obj, |
3639 | + 'org.freedesktop.DBus.Properties' |
3640 | + ) |
3641 | + self.cache = {} |
3642 | + self.ispartition = self['DeviceIsPartition'] |
3643 | + if self.ispartition: |
3644 | + self.drive = self['PartitionSlave'] |
3645 | + else: |
3646 | + self.drive = None |
3647 | + |
3648 | + def __repr__(self): |
3649 | + return '{}({!r})'.format(self.__class__.__name__, self.obj) |
3650 | + |
3651 | + def __getitem__(self, key): |
3652 | + try: |
3653 | + return self.cache[key] |
3654 | + except KeyError: |
3655 | + value = self.proxy.Get('(ss)', 'org.freedesktop.UDisks.Device', key) |
3656 | + self.cache[key] = value |
3657 | + return value |
3658 | + |
3659 | + @property |
3660 | + def ismounted(self): |
3661 | + return self['DeviceIsMounted'] |
3662 | + |
3663 | + def get_all(self): |
3664 | + return self.proxy.GetAll('(s)', 'org.freedesktop.UDisks.Device') |
3665 | + |
3666 | + def reset(self): |
3667 | + self.cache.clear() |
3668 | + |
3669 | + |
3670 | +class UDisks(GObject.GObject): |
3671 | + __gsignals__ = { |
3672 | + 'card_added': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
3673 | + [TYPE_PYOBJECT, TYPE_PYOBJECT] |
3674 | + ), |
3675 | + 'card_removed': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
3676 | + [TYPE_PYOBJECT, TYPE_PYOBJECT] |
3677 | + ), |
3678 | + 'store_removed': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
3679 | + [TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT] |
3680 | + ), |
3681 | + 'store_added': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, |
3682 | + [TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT] |
3683 | + ), |
3684 | + } |
3685 | + |
3686 | + def __init__(self): |
3687 | + super().__init__() |
3688 | + self.devices = {} |
3689 | + self.drives = {} |
3690 | + self.cards = {} |
3691 | + self.stores = {} |
3692 | + self.proxy = system.get( |
3693 | + 'org.freedesktop.UDisks', |
3694 | + '/org/freedesktop/UDisks' |
3695 | + ) |
3696 | + |
3697 | + def monitor(self): |
3698 | + user = path.abspath(os.environ['HOME']) |
3699 | + home = path.dirname(user) |
3700 | + |
3701 | + home_p = self.find(home) |
3702 | + try: |
3703 | + user_p = self.find(user) |
3704 | + except Exception: |
3705 | + user_p = home_p |
3706 | + self.special = { |
3707 | + home: home_p, |
3708 | + user: user_p, |
3709 | + } |
3710 | + self.proxy.connect('g-signal', self.on_g_signal) |
3711 | + for obj in self.proxy.EnumerateDevices(): |
3712 | + self.change_device(obj) |
3713 | + |
3714 | + def find(self, parentdir): |
3715 | + """ |
3716 | + Return DBus object path of partition containing *parentdir*. |
3717 | + """ |
3718 | + (major, minor) = major_minor(parentdir) |
3719 | + return self.proxy.FindDeviceByMajorMinor('(xx)', major, minor) |
3720 | + |
3721 | + def on_g_signal(self, proxy, sender, signal, params): |
3722 | + if signal == 'DeviceChanged': |
3723 | + self.change_device(params.unpack()[0]) |
3724 | + elif signal == 'DeviceRemoved': |
3725 | + self.remove_device(params.unpack()[0]) |
3726 | + |
3727 | + def get_device(self, obj): |
3728 | + if obj not in self.devices: |
3729 | + self.devices[obj] = Device(obj) |
3730 | + return self.devices[obj] |
3731 | + |
3732 | + def get_drive(self, obj): |
3733 | + if obj not in self.drives: |
3734 | + d = self.get_device(obj) |
3735 | + self.drives[obj] = drive_info(d) |
3736 | + return self.drives[obj] |
3737 | + |
3738 | + def change_device(self, obj): |
3739 | + d = self.get_device(obj) |
3740 | + if not d.ispartition: |
3741 | + return |
3742 | + d.reset() |
3743 | + if d.ismounted: |
3744 | + mount = usable_mount(d['DeviceMountPaths']) |
3745 | + if mount is None: |
3746 | + return |
3747 | + part = partition_info(d, mount) |
3748 | + drive = self.get_drive(d.drive) |
3749 | + drive['partitions'][obj] = part |
3750 | + store_id = get_filestore_id(mount) |
3751 | + if store_id: |
3752 | + self.add_store(obj, mount, store_id) |
3753 | + elif drive['info']['removable']: |
3754 | + self.add_card(obj, mount) |
3755 | + else: |
3756 | + try: |
3757 | + del self.drives[d.drive]['partitions'][obj] |
3758 | + if not self.drives[d.drive]['partitions']: |
3759 | + del self.drives[d.drive] |
3760 | + except KeyError: |
3761 | + pass |
3762 | + self.remove_store(obj) |
3763 | + self.remove_card(obj) |
3764 | + |
3765 | + def add_card(self, obj, mount): |
3766 | + if obj in self.cards: |
3767 | + return |
3768 | + self.cards[obj] = mount |
3769 | + log.info('card_added %r %r', obj, mount) |
3770 | + self.emit('card_added', obj, mount) |
3771 | + |
3772 | + def remove_card(self, obj): |
3773 | + try: |
3774 | + mount = self.cards.pop(obj) |
3775 | + log.info('card_removed %r %r', obj, mount) |
3776 | + self.emit('card_removed', obj, mount) |
3777 | + except KeyError: |
3778 | + pass |
3779 | + |
3780 | + def add_store(self, obj, mount, store_id): |
3781 | + if obj in self.stores: |
3782 | + return |
3783 | + self.stores[obj] = {'parentdir': mount, 'id': store_id} |
3784 | + log.info('store_added %r %r %r', obj, mount, store_id) |
3785 | + self.emit('store_added', obj, mount, store_id) |
3786 | + |
3787 | + def remove_store(self, obj): |
3788 | + try: |
3789 | + d = self.stores.pop(obj) |
3790 | + log.info('store_removed %r %r %r', obj, d['parentdir'], d['id']) |
3791 | + self.emit('store_removed', obj, d['parentdir'], d['id']) |
3792 | + except KeyError: |
3793 | + pass |
3794 | + |
3795 | + def remove_device(self, obj): |
3796 | + try: |
3797 | + del self.devices[obj] |
3798 | + except KeyError: |
3799 | + pass |
3800 | + |
3801 | + def get_parentdir_info(self, parentdir): |
3802 | + obj = self.find(parentdir) |
3803 | + d = self.get_device(obj) |
3804 | + return { |
3805 | + 'parentdir': parentdir, |
3806 | + 'partition': partition_info(d)['info'], |
3807 | + 'drive': self.get_drive(d.drive)['info'], |
3808 | + } |
3809 | + |
3810 | + def json(self): |
3811 | + d = { |
3812 | + 'drives': self.drives, |
3813 | + 'stores': self.stores, |
3814 | + 'cards': self.cards, |
3815 | + 'special': self.special, |
3816 | + } |
3817 | + return json.dumps(d, sort_keys=True, indent=4) |
3818 | + |
3819 | |
3820 | === removed directory 'dmedia/webui' |
3821 | === removed file 'dmedia/webui/__init__.py' |
3822 | --- dmedia/webui/__init__.py 2011-03-28 21:55:42 +0000 |
3823 | +++ dmedia/webui/__init__.py 1970-01-01 00:00:00 +0000 |
3824 | @@ -1,27 +0,0 @@ |
3825 | -# Authors: |
3826 | -# Jason Gerard DeRose <jderose@novacut.com> |
3827 | -# |
3828 | -# dmedia: distributed media library |
3829 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
3830 | -# |
3831 | -# This file is part of `dmedia`. |
3832 | -# |
3833 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
3834 | -# terms of the GNU Affero General Public License as published by the Free |
3835 | -# Software Foundation, either version 3 of the License, or (at your option) any |
3836 | -# later version. |
3837 | -# |
3838 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
3839 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
3840 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
3841 | -# details. |
3842 | -# |
3843 | -# You should have received a copy of the GNU Affero General Public License along |
3844 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
3845 | - |
3846 | -""" |
3847 | -Core HTML5 UI components. |
3848 | - |
3849 | -This is used both when running a web-accesible dmedia server, and when running |
3850 | -an HTML5 UI in embedded WebKit. |
3851 | -""" |
3852 | |
3853 | === removed file 'dmedia/webui/js.py' |
3854 | --- dmedia/webui/js.py 2011-04-17 23:00:44 +0000 |
3855 | +++ dmedia/webui/js.py 1970-01-01 00:00:00 +0000 |
3856 | @@ -1,552 +0,0 @@ |
3857 | -# Authors: |
3858 | -# Jason Gerard DeRose <jderose@novacut.com> |
3859 | -# |
3860 | -# dmedia: distributed media library |
3861 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
3862 | -# |
3863 | -# This file is part of `dmedia`. |
3864 | -# |
3865 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
3866 | -# terms of the GNU Affero General Public License as published by the Free |
3867 | -# Software Foundation, either version 3 of the License, or (at your option) any |
3868 | -# later version. |
3869 | -# |
3870 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
3871 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
3872 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
3873 | -# details. |
3874 | -# |
3875 | -# You should have received a copy of the GNU Affero General Public License along |
3876 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
3877 | - |
3878 | -""" |
3879 | -Allow ``unittest.TestCase`` assert methods to be called from JavaScript. |
3880 | - |
3881 | - |
3882 | -Example |
3883 | -======= |
3884 | - |
3885 | -On the Python side, you will need a `JSTestCase` subclass with one or more |
3886 | -test methods, something like this: |
3887 | - |
3888 | ->>> class TestFoo(JSTestCase): |
3889 | -... js_files = ( |
3890 | -... '/abs/path/to/script/foo.js', |
3891 | -... '/abs/path/to/script/test_foo.js', |
3892 | -... ) |
3893 | -... |
3894 | -... def test_bar(self): |
3895 | -... # 1. Optionally do something cool like initialize a Couch DB |
3896 | -... self.run_js() # 2. Call test_bar() JavaScript function |
3897 | -... # 3. Optionally do something cool like verify changes in a Couch DB |
3898 | - |
3899 | - |
3900 | -Your foo.js file (containing the ``bar()`` function) would look something like |
3901 | -this: |
3902 | - |
3903 | - :: |
3904 | - |
3905 | - function bar() { |
3906 | - return 'My JS unit testing is awesome'; |
3907 | - } |
3908 | - |
3909 | - |
3910 | -And your test_foo.js file (containing the ``test_bar()`` function) would look |
3911 | -something like this: |
3912 | - |
3913 | - :: |
3914 | - |
3915 | - py.TestFoo = { |
3916 | - test_bar: function() { |
3917 | - py.assertEqual(bar(), 'My JS unit testing is awesome'); |
3918 | - py.assertNotEqual(bar(), 'My JS unit testing is lame'); |
3919 | - }, |
3920 | - } |
3921 | - |
3922 | - |
3923 | -When you call, say, ``py.assertEqual()`` from your JavaScript test, the test |
3924 | -harness will ``JSON.stringify()`` the arguments and forward the call to the test |
3925 | -server using a synchronous XMLHttpRequest. When the main process receives this |
3926 | -request from the `ResultsApp`, the actual test is performed using |
3927 | -``TestCase.assertEqual()``. |
3928 | - |
3929 | -All the reporting is handled by the Python unit test framework, and you get an |
3930 | -overall pass/fail for the combined Python and JavaScript tests when you run: |
3931 | - |
3932 | - :: |
3933 | - |
3934 | - ./setup.py test |
3935 | - |
3936 | - |
3937 | -Oh, and the tests run headless in embedded WebKit making this viable for |
3938 | -automatic tests run with, say, Tarmac: |
3939 | - |
3940 | - https://launchpad.net/tarmac |
3941 | - |
3942 | - |
3943 | -Why this is Awesome |
3944 | -=================== |
3945 | - |
3946 | -You might be thinking, "Why reinvent the wheel, why not just use an existing |
3947 | -JavaScript unit testing framework?" |
3948 | - |
3949 | -Fair enough, but the entire client side piece for this test framework is just 39 |
3950 | -lines of JavaScript plus a bit of JSON data that gets written automatically. |
3951 | -Yet those 39 lines of JavaScript give you access to all these rich test methods: |
3952 | - |
3953 | - - ``TestCase.assertTrue()`` |
3954 | - - ``TestCase.assertFalse()`` |
3955 | - - ``TestCase.assertEqual()`` |
3956 | - - ``TestCase.assertNotEqual()`` |
3957 | - - ``TestCase.assertAlmostEqual()`` |
3958 | - - ``TestCase.assertNotAlmostEqual()`` |
3959 | - - ``TestCase.assertGreater()`` |
3960 | - - ``TestCase.assertGreaterEqual()`` |
3961 | - - ``TestCase.assertLess()`` |
3962 | - - ``TestCase.assertLessEqual()`` |
3963 | - - ``TestCase.assertIn()`` |
3964 | - - ``TestCase.assertNotIn()`` |
3965 | - - ``TestCase.assertItemsEqual()`` |
3966 | - - ``TestCase.assertIsNone()`` |
3967 | - - ``TestCase.assertIsNotNone()`` |
3968 | - - ``TestCase.assertRegexpMatches()`` |
3969 | - - ``TestCase.assertNotRegexpMatches()`` |
3970 | - |
3971 | - |
3972 | -Existing JavaScript unit test frameworks don't tend to lend themselves to |
3973 | -automatic testing as they typically report the results graphically in the |
3974 | -browser... which is totally worthless if you want to run tests as part of an |
3975 | -automatic pre-commit step. Remember what the late, great Ian Clatworthy taught |
3976 | -us: pre-commit continuous integration is 100% awesome. |
3977 | - |
3978 | -If this weren't enough, there are some reasons why dmedia and Novacut in |
3979 | -particular benefit from this type of JavaScript testing. Both use HTML5 user |
3980 | -interfaces that do everything, and I mean *everything*, by talking directly to |
3981 | -CouchDB with XMLHttpRequest. The supporting core JavaScript needs to be very |
3982 | -solid to enable great user experiences to be built atop. |
3983 | - |
3984 | -Considering the importance of these code paths, it's very handy to do setup from |
3985 | -Python before calling `JSTestCase.run_js()`... for example, putting a CouchDB |
3986 | -database into a known state. Likewise, it's very handy to do out-of-band checks |
3987 | -from Python after `JSTestCase.run_js()` completes... for example, verifying that |
3988 | -the expected changes were made to the CouchDB database. |
3989 | - |
3990 | - |
3991 | -How it Works |
3992 | -============ |
3993 | - |
3994 | -When you call `JSTestCase.run_js()`, the following happens: |
3995 | - |
3996 | - 1. The correct HTML and JavaScript for the test are prepared |
3997 | - |
3998 | - 2. The `ResultsApp` is fired up in a ``multiprocessing.Process`` |
3999 | - |
4000 | - 3. The ``dummy-client`` script is launched using ``subprocess.Popen()`` |
4001 | - |
4002 | - 4. The ``dummy-client`` requests the HTML and JavaScript from the |
4003 | - `ResultsApp` |
4004 | - |
4005 | - 5. The ``dummy-client`` executes the ``py.run()`` JavaScript function and |
4006 | - posts the results to the `ResultsApp` as the test runs |
4007 | - |
4008 | - 6. The `ResultsApp` puts the results in a ``multiprocessing.Queue`` as it |
4009 | - receives them |
4010 | - |
4011 | - 7. In the main process, `JSTestCase.collect_results()` gets results from the |
4012 | - queue and tests the assertions till the test completes, an exception is |
4013 | - raised, or the 5 second timeout is exceeded |
4014 | - |
4015 | - 8. The `JSTestCase.tearDown()` method terminates the ``dummy-client`` and |
4016 | - `ResultsApp` processes |
4017 | - |
4018 | - |
4019 | -FIXME: |
4020 | - |
4021 | - 1. Need to be able to supply arbitrary files to ResultsApp so tests can GET |
4022 | - these files... this is especially important for testing on binary data, |
4023 | - which we can't directly make available to JavaScript. We can borrow code |
4024 | - from test-server.py, which already makes this totally generic. |
4025 | -""" |
4026 | - |
4027 | - |
4028 | -from unittest import TestCase |
4029 | -import sys |
4030 | -from os import path |
4031 | -from subprocess import Popen |
4032 | -import multiprocessing |
4033 | -from Queue import Empty |
4034 | -from wsgiref.simple_server import make_server |
4035 | -import json |
4036 | -from textwrap import dedent |
4037 | - |
4038 | -from genshi.template import MarkupTemplate |
4039 | - |
4040 | -from .util import render_var |
4041 | - |
4042 | - |
4043 | -tree = path.dirname(path.dirname(path.dirname(path.abspath(__file__)))) |
4044 | -if path.exists(path.join(tree, 'setup.py')): |
4045 | - dummy_client = path.join(tree, 'dummy-client') |
4046 | -else: |
4047 | - dummy_client = path.join(sys.prefix, 'lib', 'dmedia', 'dummy-client') |
4048 | -assert path.isfile(dummy_client) |
4049 | - |
4050 | - |
4051 | -def read_input(environ): |
4052 | - try: |
4053 | - length = int(environ.get('CONTENT_LENGTH', '0')) |
4054 | - except ValueError: |
4055 | - return '' |
4056 | - return environ['wsgi.input'].read(length) |
4057 | - |
4058 | - |
4059 | -class ResultsApp(object): |
4060 | - """ |
4061 | - Simple WSGI app for collecting results from JavaScript tests. |
4062 | - |
4063 | - REST API |
4064 | - ======== |
4065 | - |
4066 | - To retrieve the test HTML page (will have appropriate JavaScript): |
4067 | - |
4068 | - :: |
4069 | - |
4070 | - GET / HTTP/1.1 |
4071 | - |
4072 | - To retrieve a JavaScript file: |
4073 | - |
4074 | - :: |
4075 | - |
4076 | - GET /scripts/foo.js HTTP/1.1 |
4077 | - |
4078 | - To test an assertion (assertEqual, assertTrue, etc): |
4079 | - |
4080 | - :: |
4081 | - |
4082 | - POST /assert HTTP/1.1 |
4083 | - Content-Type: application/json |
4084 | - |
4085 | - {"method": "assertEqual", "args": ["foo", "bar"]} |
4086 | - |
4087 | - To report an unhandled exception: |
4088 | - |
4089 | - :: |
4090 | - |
4091 | - POST /error HTTP/1.1 |
4092 | - Content-Type: application/json |
4093 | - |
4094 | - "Oh no, caught an unhandled JavaScript exception!" |
4095 | - |
4096 | - Finally, to conclude a test: |
4097 | - |
4098 | - :: |
4099 | - |
4100 | - POST /complete HTTP/1.1 |
4101 | - """ |
4102 | - def __init__(self, q, scripts, index, mime='text/html'): |
4103 | - self.q = q |
4104 | - self.scripts = scripts |
4105 | - self.index = index |
4106 | - self.mime = mime |
4107 | - |
4108 | - def __call__(self, environ, start_response): |
4109 | - method = environ['REQUEST_METHOD'] |
4110 | - if method not in ('GET', 'POST'): |
4111 | - self.q.put(('bad_method', method)) |
4112 | - start_response('405 Method Not Allowed', []) |
4113 | - return '' |
4114 | - path_info = environ['PATH_INFO'] |
4115 | - if method == 'GET': |
4116 | - if path_info == '/': |
4117 | - headers = [ |
4118 | - ('Content-Type', self.mime), |
4119 | - ('Content-Length', str(len(self.index))), |
4120 | - ] |
4121 | - self.q.put(('get', path_info)) |
4122 | - start_response('200 OK', headers) |
4123 | - return self.index |
4124 | - s = '/scripts/' |
4125 | - if path_info.startswith(s): |
4126 | - name = path_info[len(s):] |
4127 | - if name in self.scripts: |
4128 | - script = self.scripts[name] |
4129 | - headers = [ |
4130 | - ('Content-Type', 'application/javascript'), |
4131 | - ('Content-Length', str(len(script))), |
4132 | - ] |
4133 | - self.q.put(('get', path_info)) |
4134 | - start_response('200 OK', headers) |
4135 | - return script |
4136 | - self.q.put(('not_found', path_info)) |
4137 | - start_response('404 Not Found', []) |
4138 | - return '' |
4139 | - if method == 'POST': |
4140 | - if path_info == '/assert': |
4141 | - content = read_input(environ) |
4142 | - self.q.put(('assert', content)) |
4143 | - start_response('202 Accepted', []) |
4144 | - return '' |
4145 | - if path_info == '/error': |
4146 | - content = read_input(environ) |
4147 | - self.q.put(('error', content)) |
4148 | - start_response('202 Accepted', []) |
4149 | - return '' |
4150 | - if path_info == '/complete': |
4151 | - self.q.put(('complete', None)) |
4152 | - start_response('202 Accepted', []) |
4153 | - return '' |
4154 | - self.q.put( |
4155 | - ('bad_request', '%(REQUEST_METHOD)s %(PATH_INFO)s' % environ) |
4156 | - ) |
4157 | - start_response('400 Bad Request', []) |
4158 | - return '' |
4159 | - |
4160 | - |
4161 | -def results_server(q, scripts, index, mime): |
4162 | - """ |
4163 | - Start HTTP server with `ResponseApp`. |
4164 | - |
4165 | - This function is the target of a ``multiprocessing.Process`` when the |
4166 | - response server is started by `JSTestCase.start_results_server()`. |
4167 | - |
4168 | - :param q: a ``multiprocessing.Queue`` used to send results to main process |
4169 | - :param scripts: a ``dict`` mapping script names to script content |
4170 | - :param index: the HTML/XHTML to send to client |
4171 | - :param mime: the content-type of the index page, eg ``'text/html'`` |
4172 | - """ |
4173 | - app = ResultsApp(q, scripts, index, mime) |
4174 | - httpd = make_server('', 8000, app) |
4175 | - httpd.serve_forever() |
4176 | - |
4177 | - |
4178 | -class JavaScriptError(StandardError): |
4179 | - pass |
4180 | - |
4181 | - |
4182 | -class JavaScriptTimeout(StandardError): |
4183 | - pass |
4184 | - |
4185 | - |
4186 | -class InvalidTestMethod(StandardError): |
4187 | - pass |
4188 | - |
4189 | -# unittest.TestCase methods that we allow to be called from JavaScript |
4190 | -METHODS = ( |
4191 | - 'assertTrue', |
4192 | - 'assertFalse', |
4193 | - |
4194 | - 'assertEqual', |
4195 | - 'assertNotEqual', |
4196 | - 'assertAlmostEqual', |
4197 | - 'assertNotAlmostEqual', |
4198 | - |
4199 | - 'assertGreater', |
4200 | - 'assertGreaterEqual', |
4201 | - 'assertLess', |
4202 | - 'assertLessEqual', |
4203 | - |
4204 | - 'assertIn', |
4205 | - 'assertNotIn', |
4206 | - 'assertItemsEqual', |
4207 | - |
4208 | - 'assertIsNone', |
4209 | - 'assertIsNotNone', |
4210 | - |
4211 | - 'assertRegexpMatches', |
4212 | - 'assertNotRegexpMatches', |
4213 | -) |
4214 | - |
4215 | - |
4216 | -class JSTestCase(TestCase): |
4217 | - js_files = tuple() |
4218 | - q = None |
4219 | - server = None |
4220 | - client = None |
4221 | - |
4222 | - template = """ |
4223 | - <html |
4224 | - xmlns="http://www.w3.org/1999/xhtml" |
4225 | - xmlns:py="http://genshi.edgewall.org/" |
4226 | - > |
4227 | - <head> |
4228 | - <title py:content="title" /> |
4229 | - <script py:content="js_inline" type="text/javascript" /> |
4230 | - <script |
4231 | - py:for="link in js_links" |
4232 | - type="text/javascript" |
4233 | - src="${link}" |
4234 | - /> |
4235 | - </head> |
4236 | - <body onload="py.run()"> |
4237 | - <div id="example" /> |
4238 | - </body> |
4239 | - </html> |
4240 | - """ |
4241 | - |
4242 | - # The entire client-side test harness is only 39 lines of JavaScript! |
4243 | - # |
4244 | - # Note that py.data is dynamically written by JSTestCase.build_js_inline(), |
4245 | - # and that this JavaScript will be inline in the first <script> tag in the |
4246 | - # test HTML/XHTML page. |
4247 | - # |
4248 | - # The JavaScript files containing implementation and tests will follow in |
4249 | - # the order they were defined in the JSTestClass.js_files subclass |
4250 | - # attribute. |
4251 | - javascript = """ |
4252 | - var py = { |
4253 | - /* Synchronously POST results to ResultsApp */ |
4254 | - post: function(path, obj) { |
4255 | - var request = new XMLHttpRequest(); |
4256 | - request.open('POST', path, false); |
4257 | - if (obj) { |
4258 | - request.setRequestHeader('Content-Type', 'application/json'); |
4259 | - request.send(JSON.stringify(obj)); |
4260 | - } |
4261 | - else { |
4262 | - request.send(); |
4263 | - } |
4264 | - }, |
4265 | - |
4266 | - /* Initialize the py.assertFoo() functions */ |
4267 | - init: function() { |
4268 | - py.data.assertMethods.forEach(function(name) { |
4269 | - py[name] = function() { |
4270 | - var args = Array.prototype.slice.call(arguments); |
4271 | - py.post('/assert', {method: name, args: args}); |
4272 | - }; |
4273 | - }); |
4274 | - }, |
4275 | - |
4276 | - /* Run the test function indicated by py.data.methodName */ |
4277 | - run: function() { |
4278 | - try { |
4279 | - py.init(); |
4280 | - var className = py.data.className; |
4281 | - var obj = py[className]; |
4282 | - if (!obj) { |
4283 | - py.post('/error', |
4284 | - 'Missing class ' + ['py', className].join('.') |
4285 | - ); |
4286 | - } |
4287 | - else { |
4288 | - var methodName = py.data.methodName; |
4289 | - var method = obj[methodName]; |
4290 | - if (!method) { |
4291 | - py.post('/error', |
4292 | - 'Missing method ' + ['py', className, methodName].join('.') |
4293 | - ); |
4294 | - } |
4295 | - else { |
4296 | - method(); |
4297 | - } |
4298 | - } |
4299 | - } |
4300 | - catch (e) { |
4301 | - py.post('/error', e); |
4302 | - } |
4303 | - finally { |
4304 | - py.post('/complete'); |
4305 | - } |
4306 | - }, |
4307 | - }; |
4308 | - """ |
4309 | - |
4310 | - @classmethod |
4311 | - def setUpClass(cls): |
4312 | - cls.template = dedent(cls.template).strip() |
4313 | - cls.template_t = MarkupTemplate(cls.template) |
4314 | - cls.javascript = dedent(cls.javascript).strip() |
4315 | - cls.scripts = tuple(cls.load_scripts()) |
4316 | - cls.js_links = tuple( |
4317 | - '/scripts/' + name for (name, script) in cls.scripts |
4318 | - ) |
4319 | - |
4320 | - @classmethod |
4321 | - def load_scripts(cls): |
4322 | - for filename in cls.js_files: |
4323 | - yield ( |
4324 | - path.basename(filename), |
4325 | - open(filename, 'rb').read() |
4326 | - ) |
4327 | - |
4328 | - def setUp(self): |
4329 | - self.title = '%s.%s' % (self.__class__.__name__, self._testMethodName) |
4330 | - self.q = multiprocessing.Queue() |
4331 | - self.messages = [] |
4332 | - |
4333 | - def run_js(self, **extra): |
4334 | - index = self.build_page(**extra) |
4335 | - self.start_results_server(dict(self.scripts), index) |
4336 | - self.start_dummy_client() |
4337 | - self.collect_results() |
4338 | - |
4339 | - def build_data(self, **extra): |
4340 | - data = { |
4341 | - 'className': self.__class__.__name__, |
4342 | - 'methodName': self._testMethodName, |
4343 | - 'assertMethods': METHODS, |
4344 | - } |
4345 | - data.update(extra) |
4346 | - return data |
4347 | - |
4348 | - def build_js_inline(self, **extra): |
4349 | - data = self.build_data(**extra) |
4350 | - return '\n'.join([self.javascript, render_var('py.data', data, 4)]) |
4351 | - |
4352 | - def render(self, **kw): |
4353 | - return self.template_t.generate(**kw).render('xhtml', doctype='html5') |
4354 | - |
4355 | - def build_page(self, **extra): |
4356 | - kw = dict( |
4357 | - title=self.title, |
4358 | - js_inline=self.build_js_inline(**extra), |
4359 | - js_links=self.js_links, |
4360 | - ) |
4361 | - return self.render(**kw) |
4362 | - |
4363 | - def start_results_server(self, scripts, index, mime='text/html'): |
4364 | - self.server = multiprocessing.Process( |
4365 | - target=results_server, |
4366 | - args=(self.q, scripts, index, mime), |
4367 | - ) |
4368 | - self.server.daemon = True |
4369 | - self.server.start() |
4370 | - |
4371 | - def start_dummy_client(self): |
4372 | - cmd = [dummy_client, 'http://localhost:8000/'] |
4373 | - self.client = Popen(cmd) |
4374 | - |
4375 | - def collect_results(self, timeout=5): |
4376 | - while True: |
4377 | - try: |
4378 | - (action, data) = self.q.get(timeout=timeout) |
4379 | - self.messages.append((action, data)) |
4380 | - except Empty: |
4381 | - raise JavaScriptTimeout() |
4382 | - self.assertIn( |
4383 | - action, |
4384 | - ['get', 'not_found', 'assert', 'error', 'complete'] |
4385 | - ) |
4386 | - # Note that no action is taken for 'get' and 'not_found'. |
4387 | - # 'not_found' is allowed because of things like GET /favicon.ico |
4388 | - if action == 'error': |
4389 | - raise JavaScriptError(data) |
4390 | - if action == 'complete': |
4391 | - break |
4392 | - if action == 'assert': |
4393 | - d = json.loads(data) |
4394 | - if d['method'] not in METHODS: |
4395 | - raise InvalidTestMethod(data) |
4396 | - method = getattr(self, d['method']) |
4397 | - method(*d['args']) |
4398 | - |
4399 | - def tearDown(self): |
4400 | - if self.server is not None: |
4401 | - self.server.terminate() |
4402 | - self.server.join() |
4403 | - self.server = None |
4404 | - self.q = None |
4405 | - if self.client is not None: |
4406 | - self.client.terminate() |
4407 | - self.client.wait() |
4408 | - self.client = None |
4409 | |
4410 | === removed directory 'dmedia/webui/tests' |
4411 | === removed file 'dmedia/webui/tests/__init__.py' |
4412 | --- dmedia/webui/tests/__init__.py 2011-03-26 06:53:57 +0000 |
4413 | +++ dmedia/webui/tests/__init__.py 1970-01-01 00:00:00 +0000 |
4414 | @@ -1,24 +0,0 @@ |
4415 | -# Authors: |
4416 | -# Jason Gerard DeRose <jderose@novacut.com> |
4417 | -# |
4418 | -# dmedia: distributed media library |
4419 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
4420 | -# |
4421 | -# This file is part of `dmedia`. |
4422 | -# |
4423 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
4424 | -# terms of the GNU Affero General Public License as published by the Free |
4425 | -# Software Foundation, either version 3 of the License, or (at your option) any |
4426 | -# later version. |
4427 | -# |
4428 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
4429 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
4430 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
4431 | -# details. |
4432 | -# |
4433 | -# You should have received a copy of the GNU Affero General Public License along |
4434 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
4435 | - |
4436 | -""" |
4437 | -Unit tests for the `dmedia.webui` package. |
4438 | -""" |
4439 | |
4440 | === removed file 'dmedia/webui/tests/test_basejs.py' |
4441 | --- dmedia/webui/tests/test_basejs.py 2011-04-17 22:44:52 +0000 |
4442 | +++ dmedia/webui/tests/test_basejs.py 1970-01-01 00:00:00 +0000 |
4443 | @@ -1,69 +0,0 @@ |
4444 | -# Authors: |
4445 | -# Jason Gerard DeRose <jderose@novacut.com> |
4446 | -# |
4447 | -# dmedia: distributed media library |
4448 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
4449 | -# |
4450 | -# This file is part of `dmedia`. |
4451 | -# |
4452 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
4453 | -# terms of the GNU Affero General Public License as published by the Free |
4454 | -# Software Foundation, either version 3 of the License, or (at your option) any |
4455 | -# later version. |
4456 | -# |
4457 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
4458 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
4459 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
4460 | -# details. |
4461 | -# |
4462 | -# You should have received a copy of the GNU Affero General Public License along |
4463 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
4464 | - |
4465 | -""" |
4466 | -Test the base.js JavaScript. |
4467 | -""" |
4468 | - |
4469 | -from dmedia.webui.js import JSTestCase |
4470 | -from dmedia.webui.util import datafile |
4471 | - |
4472 | -class TestFunctions(JSTestCase): |
4473 | - js_files = ( |
4474 | - datafile('base.js'), |
4475 | - datafile('test_base.js'), |
4476 | - ) |
4477 | - |
4478 | - def test_dollar(self): |
4479 | - """ |
4480 | - Test the $() JavaScript function. |
4481 | - """ |
4482 | - self.run_js() |
4483 | - |
4484 | - def test_dollar_el(self): |
4485 | - """ |
4486 | - Test the $el() JavaScript function. |
4487 | - """ |
4488 | - self.run_js() |
4489 | - |
4490 | - def test_dollar_replace(self): |
4491 | - """ |
4492 | - Test the $replace() JavaScript function. |
4493 | - """ |
4494 | - self.run_js() |
4495 | - |
4496 | - def test_dollar_hide(self): |
4497 | - """ |
4498 | - Test the $hide() JavaScript function. |
4499 | - """ |
4500 | - self.run_js() |
4501 | - |
4502 | - def test_dollar_show(self): |
4503 | - """ |
4504 | - Test the $show() JavaScript function. |
4505 | - """ |
4506 | - self.run_js() |
4507 | - |
4508 | - def test_minsec(self): |
4509 | - self.run_js() |
4510 | - |
4511 | - def test_todata(self): |
4512 | - self.run_js() |
4513 | |
4514 | === removed file 'dmedia/webui/tests/test_browserjs.py' |
4515 | --- dmedia/webui/tests/test_browserjs.py 2011-04-13 05:03:16 +0000 |
4516 | +++ dmedia/webui/tests/test_browserjs.py 1970-01-01 00:00:00 +0000 |
4517 | @@ -1,38 +0,0 @@ |
4518 | -# Authors: |
4519 | -# Jason Gerard DeRose <jderose@novacut.com> |
4520 | -# |
4521 | -# dmedia: distributed media library |
4522 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
4523 | -# |
4524 | -# This file is part of `dmedia`. |
4525 | -# |
4526 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
4527 | -# terms of the GNU Affero General Public License as published by the Free |
4528 | -# Software Foundation, either version 3 of the License, or (at your option) any |
4529 | -# later version. |
4530 | -# |
4531 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
4532 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
4533 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
4534 | -# details. |
4535 | -# |
4536 | -# You should have received a copy of the GNU Affero General Public License along |
4537 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
4538 | - |
4539 | -""" |
4540 | -Test the browser.js JavaScript. |
4541 | -""" |
4542 | - |
4543 | -from dmedia.webui.js import JSTestCase |
4544 | -from dmedia.webui.util import datafile |
4545 | - |
4546 | -class TestBrowser(JSTestCase): |
4547 | - js_files = ( |
4548 | - datafile('couch.js'), |
4549 | - datafile('base.js'), |
4550 | - datafile('browser.js'), |
4551 | - datafile('test_browser.js'), |
4552 | - ) |
4553 | - |
4554 | - def test_init(self): |
4555 | - self.run_js() |
4556 | |
4557 | === removed file 'dmedia/webui/tests/test_couchjs.py' |
4558 | --- dmedia/webui/tests/test_couchjs.py 2011-04-18 02:57:38 +0000 |
4559 | +++ dmedia/webui/tests/test_couchjs.py 1970-01-01 00:00:00 +0000 |
4560 | @@ -1,97 +0,0 @@ |
4561 | -# Authors: |
4562 | -# Jason Gerard DeRose <jderose@novacut.com> |
4563 | -# |
4564 | -# dmedia: distributed media library |
4565 | -# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com> |
4566 | -# |
4567 | -# This file is part of `dmedia`. |
4568 | -# |
4569 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
4570 | -# terms of the GNU Affero General Public License as published by the Free |
4571 | -# Software Foundation, either version 3 of the License, or (at your option) any |
4572 | -# later version. |
4573 | -# |
4574 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
4575 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
4576 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
4577 | -# details. |
4578 | -# |
4579 | -# You should have received a copy of the GNU Affero General Public License along |
4580 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
4581 | - |
4582 | -""" |
4583 | -Test the couch.js JavaScript. |
4584 | -""" |
4585 | - |
4586 | -from dmedia.webui.js import JSTestCase |
4587 | -from dmedia.webui.util import datafile |
4588 | - |
4589 | - |
4590 | -class TestCouchRequest(JSTestCase): |
4591 | - js_files = ( |
4592 | - datafile('couch.js'), |
4593 | - datafile('test_couch.js'), |
4594 | - ) |
4595 | - |
4596 | - def test_request(self): |
4597 | - self.run_js() |
4598 | - |
4599 | - def test_async_request(self): |
4600 | - self.run_js() |
4601 | - |
4602 | - |
4603 | -class TestCouchBase(JSTestCase): |
4604 | - js_files = ( |
4605 | - datafile('couch.js'), |
4606 | - datafile('test_couch.js'), |
4607 | - ) |
4608 | - |
4609 | - def test_init(self): |
4610 | - self.run_js() |
4611 | - |
4612 | - def test_path(self): |
4613 | - self.run_js() |
4614 | - |
4615 | - def test_request(self): |
4616 | - self.run_js() |
4617 | - |
4618 | - def test_async_request(self): |
4619 | - self.run_js() |
4620 | - |
4621 | - def test_post(self): |
4622 | - self.run_js() |
4623 | - |
4624 | - def test_put(self): |
4625 | - self.run_js() |
4626 | - |
4627 | - def test_get(self): |
4628 | - self.run_js() |
4629 | - |
4630 | - def test_delete(self): |
4631 | - self.run_js() |
4632 | - |
4633 | - |
4634 | -class TestServer(JSTestCase): |
4635 | - js_files = ( |
4636 | - datafile('couch.js'), |
4637 | - datafile('test_couch.js'), |
4638 | - ) |
4639 | - |
4640 | - def test_database(self): |
4641 | - self.run_js() |
4642 | - |
4643 | - |
4644 | -class TestDatabase(JSTestCase): |
4645 | - js_files = ( |
4646 | - datafile('couch.js'), |
4647 | - datafile('test_couch.js'), |
4648 | - ) |
4649 | - |
4650 | - def test_save(self): |
4651 | - self.run_js() |
4652 | - |
4653 | - def test_bulksave(self): |
4654 | - self.run_js() |
4655 | - |
4656 | - def test_view(self): |
4657 | - self.run_js() |
4658 | |
4659 | === removed file 'dmedia/webui/tests/test_js.py' |
4660 | --- dmedia/webui/tests/test_js.py 2011-04-17 22:04:38 +0000 |
4661 | +++ dmedia/webui/tests/test_js.py 1970-01-01 00:00:00 +0000 |
4662 | @@ -1,491 +0,0 @@ |
4663 | -# Authors: |
4664 | -# Jason Gerard DeRose <jderose@novacut.com> |
4665 | -# |
4666 | -# dmedia: distributed media library |
4667 | -# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com> |
4668 | -# |
4669 | -# This file is part of `dmedia`. |
4670 | -# |
4671 | -# `dmedia` is free software: you can redistribute it and/or modify it under the |
4672 | -# terms of the GNU Affero General Public License as published by the Free |
4673 | -# Software Foundation, either version 3 of the License, or (at your option) any |
4674 | -# later version. |
4675 | -# |
4676 | -# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
4677 | -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
4678 | -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
4679 | -# details. |
4680 | -# |
4681 | -# You should have received a copy of the GNU Affero General Public License along |
4682 | -# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
4683 | - |
4684 | -""" |
4685 | -Unit tests for `base.js` module. |
4686 | -""" |
4687 | - |
4688 | -from unittest import TestCase |
4689 | -import json |
4690 | -import subprocess |
4691 | -import time |
4692 | -import multiprocessing |
4693 | -import multiprocessing.queues |
4694 | - |
4695 | -from dmedia.webui import js |
4696 | -from dmedia.webui.util import datafile, load_data |
4697 | -from dmedia.tests.helpers import DummyQueue, raises |
4698 | - |
4699 | - |
4700 | -class StartResponse(object): |
4701 | - status = None |
4702 | - headers = None |
4703 | - |
4704 | - def __call__(self, status, headers): |
4705 | - assert self.status is None |
4706 | - assert self.headers is None |
4707 | - self.status = status |
4708 | - self.headers = headers |
4709 | - |
4710 | - |
4711 | -class Input(object): |
4712 | - def __init__(self, content): |
4713 | - self.content = content |
4714 | - |
4715 | - def read(self, length): |
4716 | - return self.content |
4717 | - |
4718 | - |
4719 | -class test_ResultsApp(TestCase): |
4720 | - klass = js.ResultsApp |
4721 | - |
4722 | - def test_init(self): |
4723 | - q = DummyQueue() |
4724 | - scripts = { |
4725 | - 'mootools.js': 'here be mootools', |
4726 | - 'base.js': 'here be dmedia', |
4727 | - } |
4728 | - index = 'foo' |
4729 | - inst = self.klass(q, scripts, index) |
4730 | - self.assertTrue(inst.q is q) |
4731 | - self.assertTrue(inst.scripts is scripts) |
4732 | - self.assertTrue(inst.index is index) |
4733 | - self.assertEqual(inst.mime, 'text/html') |
4734 | - |
4735 | - inst = self.klass(q, scripts, index, mime='application/xhtml+xml') |
4736 | - self.assertTrue(inst.q is q) |
4737 | - self.assertTrue(inst.scripts is scripts) |
4738 | - self.assertTrue(inst.index is index) |
4739 | - self.assertEqual(inst.mime, 'application/xhtml+xml') |
4740 | - |
4741 | - def test_call(self): |
4742 | - q = DummyQueue() |
4743 | - scripts = { |
4744 | - 'mootools.js': 'here be mootools', |
4745 | - 'base.js': 'here be dmedia', |
4746 | - } |
4747 | - index = 'foo bar' |
4748 | - inst = self.klass(q, scripts, index) |
4749 | - |
4750 | - env = { |
4751 | - 'REQUEST_METHOD': 'GET', |
4752 | - 'PATH_INFO': '/', |
4753 | - } |
4754 | - sr = StartResponse() |
4755 | - self.assertEqual(inst(env, sr), 'foo bar') |
4756 | - self.assertEqual(sr.status, '200 OK') |
4757 | - self.assertEqual( |
4758 | - sr.headers, |
4759 | - [ |
4760 | - ('Content-Type', 'text/html'), |
4761 | - ('Content-Length', '7'), |
4762 | - ] |
4763 | - ) |
4764 | - self.assertEqual(q.messages, [('get', '/')]) |
4765 | - |
4766 | - post1 = json.dumps({'args': ('one', 'two'), 'method': 'assertEqual'}) |
4767 | - env = { |
4768 | - 'REQUEST_METHOD': 'POST', |
4769 | - 'PATH_INFO': '/assert', |
4770 | - 'wsgi.input': Input(post1), |
4771 | - } |
4772 | - sr = StartResponse() |
4773 | - self.assertEqual(inst(env, sr), '') |
4774 | - self.assertEqual(sr.status, '202 Accepted') |
4775 | - self.assertEqual(sr.headers, []) |
4776 | - self.assertEqual( |
4777 | - q.messages, |
4778 | - [ |
4779 | - ('get', '/'), |
4780 | - ('assert', post1), |
4781 | - ] |
4782 | - ) |
4783 | - |
4784 | - post2 = 'oh no, it no worky!' |
4785 | - env = { |
4786 | - 'REQUEST_METHOD': 'POST', |
4787 | - 'PATH_INFO': '/error', |
4788 | - 'wsgi.input': Input(post2), |
4789 | - } |
4790 | - sr = StartResponse() |
4791 | - self.assertEqual(inst(env, sr), '') |
4792 | - self.assertEqual(sr.status, '202 Accepted') |
4793 | - self.assertEqual(sr.headers, []) |
4794 | - self.assertEqual( |
4795 | - q.messages, |
4796 | - [ |
4797 | - ('get', '/'), |
4798 | - ('assert', post1), |
4799 | - ('error', post2), |
4800 | - ] |
4801 | - ) |
4802 | - |
4803 | - env = { |
4804 | - 'REQUEST_METHOD': 'POST', |
4805 | - 'PATH_INFO': '/complete', |
4806 | - } |
4807 | - sr = StartResponse() |
4808 | - self.assertEqual(inst(env, sr), '') |
4809 | - self.assertEqual(sr.status, '202 Accepted') |
4810 | - self.assertEqual(sr.headers, []) |
4811 | - self.assertEqual( |
4812 | - q.messages, |
4813 | - [ |
4814 | - ('get', '/'), |
4815 | - ('assert', post1), |
4816 | - ('error', post2), |
4817 | - ('complete', None), |
4818 | - ] |
4819 | - ) |
4820 | - |
4821 | - # Test with bad requests |
4822 | - q = DummyQueue() |
4823 | - index = 'foo bar' |
4824 | - inst = self.klass(q, scripts, index) |
4825 | - env = {'REQUEST_METHOD': 'PUT'} |
4826 | - sr = StartResponse() |
4827 | - self.assertEqual(inst(env, sr), '') |
4828 | - self.assertEqual(sr.status, '405 Method Not Allowed') |
4829 | - self.assertEqual(sr.headers, []) |
4830 | - self.assertEqual(q.messages, [('bad_method', 'PUT')]) |
4831 | - |
4832 | - env = { |
4833 | - 'REQUEST_METHOD': 'GET', |
4834 | - 'PATH_INFO': '/error', |
4835 | - } |
4836 | - sr = StartResponse() |
4837 | - self.assertEqual(inst(env, sr), '') |
4838 | - self.assertEqual(sr.status, '404 Not Found') |
4839 | - self.assertEqual(sr.headers, []) |
4840 | - self.assertEqual( |
4841 | - q.messages, |
4842 | - [ |
4843 | - ('bad_method', 'PUT'), |
4844 | - ('not_found', '/error'), |
4845 | - ] |
4846 | - ) |
4847 | - |
4848 | - env = { |
4849 | - 'REQUEST_METHOD': 'POST', |
4850 | - 'PATH_INFO': '/nope', |
4851 | - } |
4852 | - sr = StartResponse() |
4853 | - self.assertEqual(inst(env, sr), '') |
4854 | - self.assertEqual(sr.status, '400 Bad Request') |
4855 | - self.assertEqual(sr.headers, []) |
4856 | - self.assertEqual( |
4857 | - q.messages, |
4858 | - [ |
4859 | - ('bad_method', 'PUT'), |
4860 | - ('not_found', '/error'), |
4861 | - ('bad_request', 'POST /nope'), |
4862 | - ] |
4863 | - ) |
4864 | - |
4865 | - # Test script reqests |
4866 | - q = DummyQueue() |
4867 | - index = 'foo bar' |
4868 | - inst = self.klass(q, scripts, index) |
4869 | - |
4870 | - # /scripts/mootools.js |
4871 | - env = { |
4872 | - 'REQUEST_METHOD': 'GET', |
4873 | - 'PATH_INFO': '/scripts/mootools.js', |
4874 | - } |
4875 | - sr = StartResponse() |
4876 | - self.assertEqual(inst(env, sr), 'here be mootools') |
4877 | - self.assertEqual(sr.status, '200 OK') |
4878 | - self.assertEqual( |
4879 | - sr.headers, |
4880 | - [ |
4881 | - ('Content-Type', 'application/javascript'), |
4882 | - ('Content-Length', '16'), |
4883 | - ] |
4884 | - ) |
4885 | - self.assertEqual(inst.q.messages, [('get', '/scripts/mootools.js')]) |
4886 | - |
4887 | - # /scripts/base.js |
4888 | - env = { |
4889 | - 'REQUEST_METHOD': 'GET', |
4890 | - 'PATH_INFO': '/scripts/base.js', |
4891 | - } |
4892 | - sr = StartResponse() |
4893 | - self.assertEqual(inst(env, sr), 'here be dmedia') |
4894 | - self.assertEqual(sr.status, '200 OK') |
4895 | - self.assertEqual( |
4896 | - sr.headers, |
4897 | - [ |
4898 | - ('Content-Type', 'application/javascript'), |
4899 | - ('Content-Length', '14'), |
4900 | - ] |
4901 | - ) |
4902 | - self.assertEqual( |
4903 | - inst.q.messages, |
4904 | - [ |
4905 | - ('get', '/scripts/mootools.js'), |
4906 | - ('get', '/scripts/base.js'), |
4907 | - ] |
4908 | - ) |
4909 | - |
4910 | - # /scripts/foo.js |
4911 | - env = { |
4912 | - 'REQUEST_METHOD': 'GET', |
4913 | - 'PATH_INFO': '/scripts/foo.js', |
4914 | - } |
4915 | - sr = StartResponse() |
4916 | - self.assertEqual(inst(env, sr), '') |
4917 | - self.assertEqual(sr.status, '404 Not Found') |
4918 | - self.assertEqual(sr.headers, []) |
4919 | - self.assertEqual( |
4920 | - inst.q.messages, |
4921 | - [ |
4922 | - ('get', '/scripts/mootools.js'), |
4923 | - ('get', '/scripts/base.js'), |
4924 | - ('not_found', '/scripts/foo.js'), |
4925 | - ] |
4926 | - ) |
4927 | - |
4928 | - # /mootools.js |
4929 | - env = { |
4930 | - 'REQUEST_METHOD': 'GET', |
4931 | - 'PATH_INFO': '/mootools.js', |
4932 | - } |
4933 | - sr = StartResponse() |
4934 | - self.assertEqual(inst(env, sr), '') |
4935 | - self.assertEqual(sr.status, '404 Not Found') |
4936 | - self.assertEqual(sr.headers, []) |
4937 | - self.assertEqual( |
4938 | - inst.q.messages, |
4939 | - [ |
4940 | - ('get', '/scripts/mootools.js'), |
4941 | - ('get', '/scripts/base.js'), |
4942 | - ('not_found', '/scripts/foo.js'), |
4943 | - ('not_found', '/mootools.js'), |
4944 | - ] |
4945 | - ) |
4946 | - |
4947 | - |
4948 | -expected = """ |
4949 | -<!DOCTYPE html> |
4950 | -<html xmlns="http://www.w3.org/1999/xhtml"> |
4951 | -<head> |
4952 | -<title>Hello Naughty Nurse!</title> |
4953 | -<script type="text/javascript">var foo = "bar";</script> |
4954 | -<script type="text/javascript" src="/scripts/base.js"></script> |
4955 | -</head> |
4956 | -<body onload="py.run()"> |
4957 | -<div id="example"></div> |
4958 | -</body> |
4959 | -</html> |
4960 | -""".strip() |
4961 | - |
4962 | - |
4963 | -class test_JSTestCase(js.JSTestCase): |
4964 | - |
4965 | - def test_load_scripts(self): |
4966 | - klass = self.__class__ |
4967 | - self.assertEqual(list(klass.load_scripts()), []) |
4968 | - klass.js_files = ( |
4969 | - datafile('browser.js'), |
4970 | - datafile('base.js'), |
4971 | - ) |
4972 | - self.assertEqual( |
4973 | - list(klass.load_scripts()), |
4974 | - [ |
4975 | - ('browser.js', load_data(datafile('browser.js'))), |
4976 | - ('base.js', load_data(datafile('base.js'))), |
4977 | - ] |
4978 | - ) |
4979 | - |
4980 | - def test_start_results_server(self): |
4981 | - self.assertEqual( |
4982 | - self.title, 'test_JSTestCase.test_start_results_server' |
4983 | - ) |
4984 | - self.start_results_server({}, 'foo bar') |
4985 | - self.assertTrue(isinstance(self.q, multiprocessing.queues.Queue)) |
4986 | - self.assertTrue(isinstance(self.server, multiprocessing.Process)) |
4987 | - time.sleep(1) |
4988 | - self.assertTrue(self.server.daemon) |
4989 | - self.assertTrue(self.server.is_alive()) |
4990 | - self.assertEqual( |
4991 | - self.server._args, |
4992 | - (self.q, {}, 'foo bar', 'text/html') |
4993 | - ) |
4994 | - self.assertEqual(self.server._kwargs, {}) |
4995 | - self.server.terminate() |
4996 | - self.server.join() |
4997 | - |
4998 | - def test_start_dummy_client(self): |
4999 | - self.assertEqual( |
5000 | - self.title, 'test_JSTestCase.test_start_dummy_client' |
The diff has been truncated for viewing.
The UDisks change looks really good, and we need to be dunning tests. This looks good.