Merge lp:~jderose/dmedia/browser into lp:dmedia
- browser
- Merge into trunk
Proposed by
Jason Gerard DeRose
Status: | Merged |
---|---|
Approved by: | James Raymond |
Approved revision: | 313 |
Merged at revision: | 284 |
Proposed branch: | lp:~jderose/dmedia/browser |
Merge into: | lp:dmedia |
Diff against target: |
1807 lines (+590/-729) 21 files modified
dmedia-service (+1/-1) dmedia-service3 (+2/-0) dmedia-transcoder (+2/-2) dmedia/client.py (+2/-2) dmedia/extractor.py (+68/-1) dmedia/importer.py (+5/-0) dmedia/local.py (+5/-0) dmedia/server.py (+138/-12) dmedia/tests/test_server.py (+67/-1) dmedia/views.py (+55/-7) gen-proxies.py (+1/-0) relink.py (+30/-0) test-client-server.py (+6/-8) ui/browser.css (+38/-0) ui/browser.html (+32/-0) ui/browser.js (+0/-145) ui/couch.js (+0/-500) ui/index.html (+14/-4) ui/signal.js (+90/-16) ui/style.css (+34/-4) ui/test_browser.js (+0/-26) |
To merge this branch: | bzr merge lp:~jderose/dmedia/browser |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Raymond | Approve | ||
Review via email: mp+86561@code.launchpad.net |
Commit message
Description of the change
sudo make me a browser
To post a comment you must log in.
Revision history for this message
James Raymond (jamesmr) : | # |
review:
Approve
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 2011-10-24 21:22:09 +0000 |
3 | +++ dmedia-service 2011-12-21 13:10:29 +0000 |
4 | @@ -44,7 +44,7 @@ |
5 | from dc3lib.microfiber import Database, NotFound |
6 | |
7 | |
8 | -__version__ = '11.10.0' |
9 | +__version__ = '11.12.0' |
10 | BUS = 'org.freedesktop.DMedia' |
11 | IFACE = BUS |
12 | log = logging.getLogger() |
13 | |
14 | === modified file 'dmedia-service3' |
15 | --- dmedia-service3 2011-10-24 19:17:32 +0000 |
16 | +++ dmedia-service3 2011-12-21 13:10:29 +0000 |
17 | @@ -78,6 +78,8 @@ |
18 | |
19 | def run(self): |
20 | (self.httpd, self.port) = start_file_server(self.core.env) |
21 | + self._peers['self'] = 'http://localhost:{}/'.format(self.port) |
22 | + self.core.db.save(self._peers) |
23 | self.group = dbus.system.get( |
24 | 'org.freedesktop.Avahi', |
25 | self.avahi.EntryGroupNew(), |
26 | |
27 | === modified file 'dmedia-transcoder' |
28 | --- dmedia-transcoder 2011-11-26 08:48:07 +0000 |
29 | +++ dmedia-transcoder 2011-12-21 13:10:29 +0000 |
30 | @@ -272,8 +272,8 @@ |
31 | 'props': { |
32 | 'quality': 7, |
33 | 'max-keyframe-distance': 30, |
34 | - 'speed': 1, |
35 | - 'threads': 3, |
36 | + 'speed': 2, |
37 | + 'threads': 2, |
38 | }, |
39 | }, |
40 | 'filter': { |
41 | |
42 | === modified file 'dmedia/client.py' |
43 | --- dmedia/client.py 2011-10-13 08:45:15 +0000 |
44 | +++ dmedia/client.py 2011-12-21 13:10:29 +0000 |
45 | @@ -380,6 +380,6 @@ |
46 | return response |
47 | |
48 | def get(self, ch, start=0, stop=None): |
49 | - #headers = range_header(ch, start, stop) |
50 | - return self.request('GET', '/'.join([ch.id, str(start), str(stop)])) |
51 | + headers = range_header(ch, start, stop) |
52 | + return self.request('GET', ch.id, headers=headers) |
53 | |
54 | |
55 | === modified file 'dmedia/extractor.py' |
56 | --- dmedia/extractor.py 2011-12-17 04:09:19 +0000 |
57 | +++ dmedia/extractor.py 2011-12-21 13:10:29 +0000 |
58 | @@ -32,6 +32,10 @@ |
59 | from base64 import b64encode |
60 | import time |
61 | import calendar |
62 | +from collections import namedtuple |
63 | + |
64 | + |
65 | +Thumbnail = namedtuple('Thumbnail', 'content_type data') |
66 | |
67 | # exiftool adds some metadata that doesn't make sense to include: |
68 | EXIFTOOL_IGNORE = ( |
69 | @@ -205,12 +209,35 @@ |
70 | shutil.rmtree(tmp) |
71 | |
72 | |
73 | +def generate_thumbnail2(filename): |
74 | + """ |
75 | + Generate thumbnail for video at *filename*. |
76 | + """ |
77 | + try: |
78 | + tmp = tempfile.mkdtemp(prefix='dmedia.') |
79 | + dst = path.join(tmp, 'thumbnail.jpg') |
80 | + check_call([ |
81 | + 'totem-video-thumbnailer', |
82 | + '-r', # Create a "raw" thumbnail without film boarder |
83 | + '-j', # Save as JPEG instead of PNG |
84 | + '-s', '384', # 384x216 for 16:9 |
85 | + filename, |
86 | + dst, |
87 | + ]) |
88 | + return Thumbnail('image/jpeg', open(dst, 'rb').read()) |
89 | + except Exception: |
90 | + return None |
91 | + finally: |
92 | + if path.isdir(tmp): |
93 | + shutil.rmtree(tmp) |
94 | + |
95 | + |
96 | def generate_cr2_thumbnail(filename): |
97 | try: |
98 | data = check_output([ |
99 | 'ufraw-batch', |
100 | '--embedded-image', |
101 | - '--size=384', |
102 | + '--size=324', |
103 | '--compression=85', |
104 | '--out-type=jpg', |
105 | '--output=-', |
106 | @@ -224,6 +251,22 @@ |
107 | pass |
108 | |
109 | |
110 | +def generate_cr2_thumbnail2(filename): |
111 | + try: |
112 | + data = check_output([ |
113 | + 'ufraw-batch', |
114 | + '--embedded-image', |
115 | + '--size=324', # 324x216 for 3:2 |
116 | + '--compression=85', |
117 | + '--out-type=jpg', |
118 | + '--output=-', |
119 | + filename, |
120 | + ]) |
121 | + return Thumbnail('image/jpeg', data) |
122 | + except CalledProcessError: |
123 | + pass |
124 | + |
125 | + |
126 | #### High-level meta-data extract/merge functions: |
127 | |
128 | _extractors = {} |
129 | @@ -253,6 +296,18 @@ |
130 | doc['meta'] = meta |
131 | |
132 | |
133 | +def merge_exif2(src): |
134 | + exif = extract_exif(src) |
135 | + for (key, values) in EXIF_REMAP.items(): |
136 | + for v in values: |
137 | + if v in exif: |
138 | + yield (key, exif[v]) |
139 | + break |
140 | + mtime = extract_mtime_from_exif(exif) |
141 | + if mtime is not None: |
142 | + yield ('mtime', mtime) |
143 | + |
144 | + |
145 | def merge_exif(src, attachments): |
146 | exif = extract_exif(src) |
147 | for (key, values) in EXIF_REMAP.items(): |
148 | @@ -271,6 +326,18 @@ |
149 | register(merge_exif, 'jpg', 'png', 'cr2') |
150 | |
151 | |
152 | +def merge_video_info2(src): |
153 | + info = extract_video_info(src) |
154 | + for (dst_key, src_key) in TOTEM_REMAP: |
155 | + if src_key in info: |
156 | + value = info[src_key] |
157 | + try: |
158 | + value = int(value) |
159 | + except ValueError: |
160 | + pass |
161 | + yield (dst_key, value) |
162 | + |
163 | + |
164 | def merge_video_info(src, attachments): |
165 | info = extract_video_info(src) |
166 | for (dst_key, src_key) in TOTEM_REMAP: |
167 | |
168 | === modified file 'dmedia/importer.py' |
169 | --- dmedia/importer.py 2011-12-17 12:15:58 +0000 |
170 | +++ dmedia/importer.py 2011-12-21 13:10:29 +0000 |
171 | @@ -32,6 +32,7 @@ |
172 | from gettext import ngettext |
173 | from subprocess import check_call |
174 | import logging |
175 | +import mimetypes |
176 | |
177 | import microfiber |
178 | from filestore import FileStore, scandir, batch_import_iter, statvfs |
179 | @@ -42,6 +43,7 @@ |
180 | |
181 | |
182 | log = logging.getLogger() |
183 | +mimetypes.init() |
184 | |
185 | |
186 | def normalize_ext(filename): |
187 | @@ -261,6 +263,9 @@ |
188 | ext = normalize_ext(file.name) |
189 | if ext: |
190 | doc['ext'] = ext |
191 | + (mime, encoding) = mimetypes.guess_type(file.name) |
192 | + if mime: |
193 | + doc['content_type'] = mime |
194 | if self.extract: |
195 | merge_metadata(file.name, doc) |
196 | yield ('new', file, doc) |
197 | |
198 | === modified file 'dmedia/local.py' |
199 | --- dmedia/local.py 2011-10-24 19:17:32 +0000 |
200 | +++ dmedia/local.py 2011-12-21 13:10:29 +0000 |
201 | @@ -187,3 +187,8 @@ |
202 | fs = self.stores.choose_local_store(doc) |
203 | return fs.stat(_id) |
204 | |
205 | + def stat2(self, doc): |
206 | + self.update_stores() |
207 | + fs = self.stores.choose_local_store(doc) |
208 | + return fs.stat(doc['_id']) |
209 | + |
210 | |
211 | === modified file 'dmedia/server.py' |
212 | --- dmedia/server.py 2011-10-12 23:40:17 +0000 |
213 | +++ dmedia/server.py 2011-12-21 13:10:29 +0000 |
214 | @@ -86,6 +86,10 @@ |
215 | status = '412 Precondition Failed' |
216 | |
217 | |
218 | +class BadRangeRequest(HTTPError): |
219 | + status = '416 Requested Range Not Satisfiable' |
220 | + |
221 | + |
222 | def get_slice(environ): |
223 | parts = environ['PATH_INFO'].lstrip('/').split('/') |
224 | if len(parts) > 3: |
225 | @@ -108,6 +112,84 @@ |
226 | return (_id, start, stop) |
227 | |
228 | |
229 | +def range_to_slice(value): |
230 | + """ |
231 | + Convert from HTTP Range request to Python slice semantics. |
232 | + |
233 | + Python slice semantics are quite natural to deal with, whereas the HTTP |
234 | + Range semantics are a touch wacky, so this function will help prevent silly |
235 | + errors. |
236 | + |
237 | + For example, say we're requesting parts of a 10,000 byte long file. This |
238 | + requests the first 500 bytes: |
239 | + |
240 | + >>> range_to_slice('bytes=0-499') |
241 | + (0, 500) |
242 | + |
243 | + This requests the second 500 bytes: |
244 | + |
245 | + >>> range_to_slice('bytes=500-999') |
246 | + (500, 1000) |
247 | + |
248 | + All three of these request the final 500 bytes: |
249 | + |
250 | + >>> range_to_slice('bytes=9500-9999') |
251 | + (9500, 10000) |
252 | + >>> range_to_slice('bytes=-500') |
253 | + (-500, None) |
254 | + >>> range_to_slice('bytes=9500-') |
255 | + (9500, None) |
256 | + |
257 | + For details on HTTP Range header, see: |
258 | + |
259 | + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35 |
260 | + """ |
261 | + unit = 'bytes=' |
262 | + if not value.startswith(unit): |
263 | + raise BadRangeRequest('bad range units') |
264 | + value = value[len(unit):] |
265 | + if value.startswith('-'): |
266 | + try: |
267 | + return (int(value), None) |
268 | + except ValueError: |
269 | + raise BadRangeRequest('range -start is not an integer') |
270 | + parts = value.split('-') |
271 | + if not len(parts) == 2: |
272 | + raise BadRangeRequest('not formatted as bytes=start-end') |
273 | + try: |
274 | + start = int(parts[0]) |
275 | + except ValueError: |
276 | + raise BadRangeRequest('range start is not an integer') |
277 | + try: |
278 | + end = parts[1] |
279 | + stop = (int(end) + 1 if end else None) |
280 | + except ValueError: |
281 | + raise BadRangeRequest('range end is not an integer') |
282 | + if not (stop is None or start < stop): |
283 | + raise BadRangeRequest('range end must be less than or equal to start') |
284 | + return (start, stop) |
285 | + |
286 | + |
287 | +def slice_to_content_range(start, stop, length): |
288 | + """ |
289 | + Convert Python slice to HTTP Content-Range. |
290 | + |
291 | + For example, a slice containing the first 500 bytes of a 1234 byte file: |
292 | + |
293 | + >>> slice_to_content_range(0, 500, 1234) |
294 | + 'bytes 0-499/1234' |
295 | + |
296 | + Or the 2nd 500 bytes: |
297 | + |
298 | + >>> slice_to_content_range(500, 1000, 1234) |
299 | + 'bytes 500-999/1234' |
300 | + |
301 | + """ |
302 | + assert 0 <= start < length |
303 | + assert start < stop <= length |
304 | + return 'bytes {}-{}/{}'.format(start, stop - 1, length) |
305 | + |
306 | + |
307 | class BaseWSGIMeta(type): |
308 | def __new__(meta, name, bases, dict): |
309 | http_methods = [] |
310 | @@ -132,11 +214,34 @@ |
311 | return [e.body] |
312 | |
313 | |
314 | +MiB = 1024 * 1024 |
315 | + |
316 | + |
317 | +class FileSlice: |
318 | + __slots__ = ('fp', 'start', 'stop') |
319 | + |
320 | + def __init__(self, fp, start=0, stop=None): |
321 | + self.fp = fp |
322 | + self.start = start |
323 | + self.stop = stop |
324 | + |
325 | + def __iter__(self): |
326 | + self.fp.seek(self.start) |
327 | + remaining = self.stop - self.start |
328 | + while remaining: |
329 | + read = min(remaining, MiB) |
330 | + remaining -= read |
331 | + data = self.fp.read(read) |
332 | + assert len(data) == read |
333 | + yield data |
334 | + assert remaining == 0 |
335 | + |
336 | + |
337 | class ReadOnlyApp(BaseWSGI): |
338 | def __init__(self, env): |
339 | self.local = local.LocalSlave(env) |
340 | info = { |
341 | - 'dmedia': 'Welcome', |
342 | + 'Dmedia': 'Welcome', |
343 | 'version': __version__, |
344 | 'machine_id': env.get('machine_id'), |
345 | } |
346 | @@ -147,21 +252,43 @@ |
347 | return [self._info] |
348 | |
349 | def GET(self, environ, start_response): |
350 | - if environ['PATH_INFO'] == '/': |
351 | + path_info = environ['PATH_INFO'] |
352 | + if path_info == '/': |
353 | return self.server_info(environ, start_response) |
354 | - # FIXME: Also validate slice compared to file-size |
355 | - (_id, start, stop) = get_slice(environ) |
356 | + |
357 | + _id = path_info.lstrip('/') |
358 | + if not (len(_id) == DIGEST_B32LEN and set(_id).issubset(B32ALPHABET)): |
359 | + raise NotFound() |
360 | try: |
361 | - st = self.local.stat(_id) |
362 | + doc = self.local.get_doc(_id) |
363 | + st = self.local.stat2(doc) |
364 | fp = open(st.name, 'rb') |
365 | except Exception: |
366 | raise NotFound() |
367 | - _start = start * LEAF_SIZE |
368 | - _stop = (st.size if stop is None else min(st.size, stop * LEAF_SIZE)) |
369 | - fp.seek(_start) |
370 | - length = str(_stop - _start) |
371 | - start_response('200 OK', [('Content-Length', length)]) |
372 | - return environ['wsgi.file_wrapper'](fp) |
373 | + |
374 | + if doc.get('content_type'): |
375 | + headers = [('Content-Type', doc['content_type'])] |
376 | + else: |
377 | + headers = [] |
378 | + |
379 | + if 'HTTP_RANGE' in environ: |
380 | + (start, stop) = range_to_slice(environ['HTTP_RANGE']) |
381 | + status = '206 Partial Content' |
382 | + else: |
383 | + start = 0 |
384 | + stop = None |
385 | + status = '200 OK' |
386 | + |
387 | + stop = (st.size if stop is None else min(st.size, stop)) |
388 | + length = str(stop - start) |
389 | + headers.append(('Content-Length', length)) |
390 | + if 'HTTP_RANGE' in environ: |
391 | + headers.append( |
392 | + ('Content-Range', slice_to_content_range(start, stop, st.size)) |
393 | + ) |
394 | + |
395 | + start_response(status, headers) |
396 | + return FileSlice(fp, start, stop) |
397 | |
398 | |
399 | class ReadWriteApp(ReadOnlyApp): |
400 | @@ -171,4 +298,3 @@ |
401 | def POST(self, environ, start_response): |
402 | pass |
403 | |
404 | - |
405 | |
406 | === modified file 'dmedia/tests/test_server.py' |
407 | --- dmedia/tests/test_server.py 2011-09-27 01:13:14 +0000 |
408 | +++ dmedia/tests/test_server.py 2011-12-21 13:10:29 +0000 |
409 | @@ -28,7 +28,7 @@ |
410 | from filestore import DIGEST_B32LEN, DIGEST_BYTES |
411 | from microfiber import random_id |
412 | |
413 | -from dmedia import server |
414 | +from dmedia import server, client |
415 | |
416 | |
417 | class StartResponse: |
418 | @@ -179,6 +179,72 @@ |
419 | server.get_slice({'PATH_INFO': bad}) |
420 | self.assertEqual(cm.exception.body, b'start must be less than stop') |
421 | |
422 | + def test_range_to_slice(self): |
423 | + with self.assertRaises(server.BadRangeRequest) as cm: |
424 | + (start, stop) = server.range_to_slice('goats=0-500') |
425 | + self.assertEqual(cm.exception.body, b'bad range units') |
426 | + |
427 | + with self.assertRaises(server.BadRangeRequest) as cm: |
428 | + (start, stop) = server.range_to_slice('bytes=-500-999') |
429 | + self.assertEqual(cm.exception.body, b'range -start is not an integer') |
430 | + |
431 | + with self.assertRaises(server.BadRangeRequest) as cm: |
432 | + (start, stop) = server.range_to_slice('bytes=-foo') |
433 | + self.assertEqual(cm.exception.body, b'range -start is not an integer') |
434 | + |
435 | + with self.assertRaises(server.BadRangeRequest) as cm: |
436 | + (start, stop) = server.range_to_slice('bytes=500') |
437 | + self.assertEqual(cm.exception.body, b'not formatted as bytes=start-end') |
438 | + |
439 | + with self.assertRaises(server.BadRangeRequest) as cm: |
440 | + (start, stop) = server.range_to_slice('bytes=foo-999') |
441 | + self.assertEqual(cm.exception.body, b'range start is not an integer') |
442 | + |
443 | + with self.assertRaises(server.BadRangeRequest) as cm: |
444 | + (start, stop) = server.range_to_slice('bytes=500-bar') |
445 | + self.assertEqual(cm.exception.body, b'range end is not an integer') |
446 | + |
447 | + with self.assertRaises(server.BadRangeRequest) as cm: |
448 | + (start, stop) = server.range_to_slice('bytes=500-499') |
449 | + self.assertEqual( |
450 | + cm.exception.body, |
451 | + b'range end must be less than or equal to start' |
452 | + ) |
453 | + |
454 | + self.assertEqual( |
455 | + server.range_to_slice('bytes=0-0'), (0, 1) |
456 | + ) |
457 | + self.assertEqual( |
458 | + server.range_to_slice('bytes=0-499'), (0, 500) |
459 | + ) |
460 | + self.assertEqual( |
461 | + server.range_to_slice('bytes=500-999'), (500, 1000) |
462 | + ) |
463 | + self.assertEqual( |
464 | + server.range_to_slice('bytes=9500-9999'), (9500, 10000) |
465 | + ) |
466 | + self.assertEqual( |
467 | + server.range_to_slice('bytes=9500-'), (9500, None) |
468 | + ) |
469 | + self.assertEqual( |
470 | + server.range_to_slice('bytes=-500'), (-500, None) |
471 | + ) |
472 | + |
473 | + # Test the round-trip with client.bytes_range |
474 | + slices = [ |
475 | + (0, 1), |
476 | + (0, 500), |
477 | + (500, 1000), |
478 | + (9500, 10000), |
479 | + (-500, None), |
480 | + (9500, None), |
481 | + ] |
482 | + for (start, stop) in slices: |
483 | + self.assertEqual( |
484 | + server.range_to_slice(client.bytes_range(start, stop)), |
485 | + (start, stop) |
486 | + ) |
487 | + |
488 | |
489 | class TestBaseWSGI(TestCase): |
490 | def test_metaclass(self): |
491 | |
492 | === modified file 'dmedia/views.py' |
493 | --- dmedia/views.py 2011-11-26 09:24:37 +0000 |
494 | +++ dmedia/views.py 2011-12-21 13:10:29 +0000 |
495 | @@ -107,7 +107,7 @@ |
496 | if (doc.type == 'dmedia/file') { |
497 | var key; |
498 | for (key in doc.stored) { |
499 | - emit(key, doc.bytes); |
500 | + emit(key, [1, doc.bytes]); |
501 | } |
502 | } |
503 | } |
504 | @@ -116,7 +116,7 @@ |
505 | file_origin = """ |
506 | function(doc) { |
507 | if (doc.type == 'dmedia/file') { |
508 | - emit(doc.origin, doc.bytes); |
509 | + emit(doc.origin, [1, doc.bytes]); |
510 | } |
511 | } |
512 | """ |
513 | @@ -207,7 +207,7 @@ |
514 | file_ext = """ |
515 | function(doc) { |
516 | if (doc.type == 'dmedia/file') { |
517 | - emit(doc.ext, null); |
518 | + emit(doc.ext, [1, doc.bytes]); |
519 | } |
520 | } |
521 | """ |
522 | @@ -258,6 +258,37 @@ |
523 | """ |
524 | |
525 | |
526 | +user_video = """ |
527 | +function(doc) { |
528 | + if (doc.type == 'dmedia/file' && doc.origin == 'user') { |
529 | + if (doc.ext == 'mov') { |
530 | + emit(doc.ctime, doc.bytes); |
531 | + } |
532 | + } |
533 | +} |
534 | +""" |
535 | + |
536 | +user_audio = """ |
537 | +function(doc) { |
538 | + if (doc.type == 'dmedia/file' && doc.origin == 'user') { |
539 | + if (doc.ext == 'wav') { |
540 | + emit(doc.ctime, doc.bytes); |
541 | + } |
542 | + } |
543 | +} |
544 | +""" |
545 | + |
546 | +user_photo = """ |
547 | +function(doc) { |
548 | + if (doc.type == 'dmedia/file' && doc.origin == 'user') { |
549 | + if (['cr2', 'jpg'].indexOf(doc.ext) >= 0) { |
550 | + emit(doc.ctime, doc.bytes); |
551 | + } |
552 | + } |
553 | +} |
554 | +""" |
555 | + |
556 | + |
557 | partition_uuid = """ |
558 | function(doc) { |
559 | if (doc.type == 'dmedia/partition') { |
560 | @@ -282,6 +313,20 @@ |
561 | } |
562 | """ |
563 | |
564 | +# Reduce function to both count and sum in a single view (thanks manveru!) |
565 | +_both = """ |
566 | +function(key, values, rereduce) { |
567 | + var count = 0; |
568 | + var sum = 0; |
569 | + var i; |
570 | + for (i in values) { |
571 | + count += values[i][0]; |
572 | + sum += values[i][1]; |
573 | + } |
574 | + return [count, sum]; |
575 | +} |
576 | +""" |
577 | + |
578 | |
579 | designs = ( |
580 | ('doc', ( |
581 | @@ -300,7 +345,10 @@ |
582 | )), |
583 | |
584 | ('file', ( |
585 | - ('stored', file_stored, _sum), |
586 | + ('stored', file_stored, _both), |
587 | + ('ext', file_ext, _both), |
588 | + ('origin', file_origin, _both), |
589 | + |
590 | ('fragile', file_fragile, None), |
591 | ('reclaimable', file_reclaimable, None), |
592 | ('partial', file_partial, _sum), |
593 | @@ -308,9 +356,6 @@ |
594 | ('bytes', file_bytes, _sum), |
595 | ('verified', file_verified, None), |
596 | ('ctime', file_ctime, None), |
597 | - ('ext', file_ext, _count), |
598 | - ('origin-count', file_origin, _count), |
599 | - ('origin-bytes', file_origin, _sum), |
600 | )), |
601 | |
602 | ('user', ( |
603 | @@ -318,6 +363,9 @@ |
604 | ('tags', user_tags, _count), |
605 | ('ctime', user_ctime, None), |
606 | ('needsproxy', user_needsproxy, None), |
607 | + ('video', user_video, _sum), |
608 | + ('photo', user_photo, _sum), |
609 | + ('audio', user_audio, _sum), |
610 | )), |
611 | |
612 | ('store', ( |
613 | |
614 | === modified file 'gen-proxies.py' |
615 | --- gen-proxies.py 2011-11-26 09:24:37 +0000 |
616 | +++ gen-proxies.py 2011-12-21 13:10:29 +0000 |
617 | @@ -37,6 +37,7 @@ |
618 | proxy = create_file(ch.id, ch.file_size, ch.leaf_hashes, stored, 'proxy') |
619 | proxy['proxyof'] = _id |
620 | proxy['content_type'] = 'video/webm' |
621 | + proxy['ext'] = 'webm' |
622 | proxy['elapsed'] = elapsed |
623 | db.save(proxy) |
624 | doc = db.get(_id) |
625 | |
626 | === added file 'relink.py' |
627 | --- relink.py 1970-01-01 00:00:00 +0000 |
628 | +++ relink.py 2011-12-21 13:10:29 +0000 |
629 | @@ -0,0 +1,30 @@ |
630 | +#!/usr/bin/python3 |
631 | + |
632 | +import sys |
633 | +from os import path |
634 | + |
635 | +from microfiber import Database, dc3_env, Conflict, NotFound |
636 | +from dmedia.core import init_filestore |
637 | + |
638 | +parentdir = path.abspath(sys.argv[1]) |
639 | + |
640 | +db = Database('dmedia', dc3_env()) |
641 | +(fs, doc) = init_filestore(parentdir) |
642 | +try: |
643 | + db.save(doc) |
644 | +except Conflict: |
645 | + pass |
646 | + |
647 | +for st in fs: |
648 | + print(st.id) |
649 | + try: |
650 | + doc = db.get(st.id) |
651 | + doc['stored'][fs.id] = { |
652 | + 'copies': 1, |
653 | + 'mtime': st.mtime, |
654 | + 'plugin': 'filestore', |
655 | + } |
656 | + db.save(doc) |
657 | + except NotFound: |
658 | + pass |
659 | + |
660 | |
661 | === modified file 'test-client-server.py' |
662 | --- test-client-server.py 2011-10-14 04:12:14 +0000 |
663 | +++ test-client-server.py 2011-12-21 13:10:29 +0000 |
664 | @@ -2,6 +2,7 @@ |
665 | |
666 | from microfiber import dc3_env |
667 | from filestore import FileStore |
668 | +import time |
669 | |
670 | from dmedia.core import Core, start_file_server |
671 | from dmedia.tests.base import TempDir |
672 | @@ -20,14 +21,11 @@ |
673 | ch = core.content_hash(row['id']) |
674 | print(ch.id) |
675 | dw = DownloadWriter(ch, dst) |
676 | - while True: |
677 | - try: |
678 | - (start, stop) = dw.next_slice() |
679 | - response = client.get(ch, start, stop) |
680 | - for leaf in threaded_response_iter(response, start=start): |
681 | - print(leaf.index, dw.write_leaf(leaf)) |
682 | - except DownloadComplete: |
683 | - break |
684 | + (start, stop) = dw.next_slice() |
685 | + for i in range(stop): |
686 | + response = client.get(ch, i, i+1) |
687 | + for leaf in threaded_response_iter(response, start=i): |
688 | + print(leaf.index, dw.write_leaf(leaf)) |
689 | dw.finish() |
690 | dst.remove(ch.id) # So we don't fill up /tmp |
691 | |
692 | |
693 | === added file 'ui/browser.css' |
694 | --- ui/browser.css 1970-01-01 00:00:00 +0000 |
695 | +++ ui/browser.css 2011-12-21 13:10:29 +0000 |
696 | @@ -0,0 +1,38 @@ |
697 | +body { |
698 | + background-color: #333; |
699 | + color: #fff; |
700 | + font-family: Ubuntu; |
701 | + font-size: 15px; |
702 | + padding: 0; |
703 | + margin: 0; |
704 | +} |
705 | + |
706 | +video { |
707 | + background-color: #999; |
708 | +} |
709 | + |
710 | +#tray { |
711 | + position: fixed; |
712 | + right: 0; |
713 | + top: 0; |
714 | + bottom: 0; |
715 | + width: 185px; |
716 | + background-color: #000; |
717 | + overflow-y: scroll; |
718 | +} |
719 | + |
720 | +img { |
721 | + display: block; |
722 | + border: 5px solid #000; |
723 | +} |
724 | + |
725 | +img.selected { |
726 | + border-color: #fff; |
727 | +} |
728 | + |
729 | + |
730 | +th { |
731 | + font-weight: normal; |
732 | + text-align: right; |
733 | +} |
734 | + |
735 | |
736 | === added file 'ui/browser.html' |
737 | --- ui/browser.html 1970-01-01 00:00:00 +0000 |
738 | +++ ui/browser.html 2011-12-21 13:10:29 +0000 |
739 | @@ -0,0 +1,32 @@ |
740 | +<!doctype html> |
741 | +<html> |
742 | +<head> |
743 | +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
744 | +<link rel="stylesheet" href="browser.css"/> |
745 | +<script src="/_apps/dc3/couch.js"></script> |
746 | +<script src="/_apps/dc3/base.js"></script> |
747 | +<script src="browser.js"></script> |
748 | +<body> |
749 | +<video width="640" height="360" id="player" controls></video> |
750 | +<div id="info"> |
751 | +<table> |
752 | +<tr> |
753 | +<th>Camera:</th><td id="camera"></td> |
754 | +</tr> |
755 | +<tr> |
756 | +<th>Lens:</th><td id="lens"></td> |
757 | +</tr> |
758 | +<tr> |
759 | +<th>Aperture:</th><td id="aperture"></td> |
760 | +</tr> |
761 | +<tr> |
762 | +<th>Shutter:</th><td id="shutter"></td> |
763 | +</tr> |
764 | +<tr> |
765 | +<th>ISO:</th><td id="iso"></td> |
766 | +</tr> |
767 | +</table> |
768 | +</div> |
769 | +<div id="tray"></div> |
770 | +</body> |
771 | +</html> |
772 | |
773 | === added file 'ui/browser.js' |
774 | --- ui/browser.js 1970-01-01 00:00:00 +0000 |
775 | +++ ui/browser.js 2011-12-21 13:10:29 +0000 |
776 | @@ -0,0 +1,63 @@ |
777 | +"use strict"; |
778 | + |
779 | +var db = new couch.Database('dmedia'); |
780 | + |
781 | +var UI = { |
782 | + on_view: function(req) { |
783 | + var rows = req.read()['rows']; |
784 | + var tray = $('tray'); |
785 | + rows.forEach(function(row) { |
786 | + var id = row.id; |
787 | + var img = $el('img', |
788 | + { |
789 | + id: id, |
790 | + src: db.att_url(id, 'thumbnail'), |
791 | + width: 160, |
792 | + height: 90, |
793 | + } |
794 | + ); |
795 | + img._value = row.value; |
796 | + img.onclick = function() { |
797 | + UI.play(id); |
798 | + } |
799 | + tray.appendChild(img); |
800 | + }); |
801 | + }, |
802 | + |
803 | + on_doc: function(req) { |
804 | + var doc = req.read(); |
805 | + var d = new Date(doc.ctime * 1000); |
806 | + var keys = ['camera', 'lens', 'aperture', 'shutter', 'iso']; |
807 | + keys.forEach(function(key) { |
808 | + $(key).textContent = doc.meta[key]; |
809 | + }); |
810 | + }, |
811 | + |
812 | + play: function(id) { |
813 | + if (UI.selected) { |
814 | + UI.selected.classList.remove('selected'); |
815 | + } |
816 | + UI.selected = $(id); |
817 | + UI.selected.classList.add('selected'); |
818 | + UI.player.src = UI.url + id; |
819 | + UI.player.load(); |
820 | + UI.player.play(); |
821 | + db.get(UI.on_doc, id); |
822 | + }, |
823 | + |
824 | + next: function() { |
825 | + if (UI.selected && UI.selected.nextSibling) { |
826 | + UI.play(UI.selected.nextSibling.id); |
827 | + } |
828 | + }, |
829 | +} |
830 | + |
831 | +window.onload = function() { |
832 | + UI.url = db.get_sync('_local/peers')['self']; |
833 | + UI.info = $('info'); |
834 | + UI.player = $('player'); |
835 | + UI.player.addEventListener('ended', function() { |
836 | + UI.next(); |
837 | + }); |
838 | + db.view(UI.on_view, 'user', 'video'); |
839 | +} |
840 | |
841 | === removed file 'ui/browser.js' |
842 | --- ui/browser.js 2011-05-04 14:00:29 +0000 |
843 | +++ ui/browser.js 1970-01-01 00:00:00 +0000 |
844 | @@ -1,145 +0,0 @@ |
845 | -"use strict"; |
846 | - |
847 | - |
848 | -function Browser(id, db) { |
849 | - this.el = $(id); |
850 | - this.db = db; |
851 | - this.display = $('display'); |
852 | -} |
853 | -Browser.prototype = { |
854 | - run: function() { |
855 | - var r = this.db.view('user', 'video', |
856 | - {include_docs: true, descending: true} |
857 | - ); |
858 | - this.load(r.rows); |
859 | - }, |
860 | - |
861 | - load: function(rows) { |
862 | - rows.forEach(function(r) { |
863 | - var self = this; |
864 | - var doc = r.doc; |
865 | - |
866 | - var div = $el('div', {'class': 'item'}); |
867 | - var img = $el('img', {'width': 160, 'height': 90}); |
868 | - if (doc._attachments.thumbnail) { |
869 | - img.setAttribute('src', this.db.path([doc._id, 'thumbnail'])); |
870 | - } |
871 | - img.onclick = function() { |
872 | - self.get_data(doc); |
873 | - }; |
874 | - |
875 | - var time = $el('div', {'class': 'time'}); |
876 | - time.textContent = doc.meta.duration + 's'; |
877 | - |
878 | - div.appendChild(img); |
879 | - div.appendChild(time); |
880 | - div.appendChild($el('div', {'class': 'star_off'})); |
881 | - div.appendChild($el('div', {'class': 'star_on'})); |
882 | - this.el.appendChild(div); |
883 | - }, this); |
884 | - }, |
885 | - |
886 | - get_data: function(doc) { |
887 | - var el = $('meta.name'); |
888 | - if (el) { |
889 | - el.textContent = doc.name; |
890 | - } |
891 | - var names = ['iso', 'aperture', 'shutter', 'focal_length', |
892 | - 'lens', 'camera']; |
893 | - names.forEach(function(n) { |
894 | - var el = $('meta.' + n); |
895 | - if (el) { |
896 | - el.textContent = doc.meta[n]; |
897 | - } |
898 | - }); |
899 | - }, |
900 | - |
901 | - |
902 | -} |
903 | - |
904 | -var selected = "none"; |
905 | - |
906 | - |
907 | - |
908 | -function get_data(doc){ |
909 | - //console.log('get_data', doc); |
910 | - |
911 | -// if (selected != "none"){ |
912 | -// //if something is selected, save the changes to it's values before loading the next object |
913 | -// form = document.forms[0]; |
914 | -// title = form.elements["title"]; |
915 | -// tags = form.elements["tags"]; |
916 | -// description = form.elements["description"]; |
917 | -// notes = form.elements["notes"]; |
918 | -// set_data(selected, "title", title.value); |
919 | -// set_data(selected, "tags", tags.value); |
920 | -// set_data(selected, "description", description.value); |
921 | -// set_data(selected, "notes", notes.value); |
922 | -// }; |
923 | -// selected = doc._id; |
924 | - |
925 | - var preview = document.getElementById('display'); |
926 | - var content = "<h2>Video Info</h2><p>"; |
927 | - content += "<b>File Name: </b>" + doc.basename + "<br>"; |
928 | - content += "<b>FPS: </b>" + doc.fps + "<br>"; |
929 | - content += "<b>Aperture: </b>" + doc.aperture + "<br>"; |
930 | - content += "<b>Focal Length: </b>" + doc.focal_length + "<br>"; |
931 | - content += "<b>Shutter: </b>" + doc.shutter + "<br>"; |
932 | - content += "<b>Camera: </b>" + doc.camera + "<br>"; |
933 | - content += "<b>Video Codec: </b>" + doc.codec_video + "<br>"; |
934 | - content += "<b>Resolution: </b>" + doc.width + "x" + doc.height + "<br>"; |
935 | - //info.innerHTML = content; |
936 | - |
937 | - content += "<img src=\"data:"; |
938 | - content += doc._attachments.thumbnail.content_type; |
939 | - content += ";base64,"; |
940 | - content += doc._attachments.thumbnail.data; |
941 | - content += "\" width=\"192\" height=\"108\" align=\"center\"><br>"; |
942 | - |
943 | - content += "<form>"; |
944 | - content += "<b>Title: </b><input type=\"text\" class=\"field\" name=\"title\" value=\"" + doc.title + "\"><br>"; |
945 | - content += "<div class=\"star " + doc.rating + "\">" + doc.rating + "</div>"; |
946 | - content += "<b>Tags: </b><br><textarea class=\"field\" name=\"tags\" rows=\"5\" cols=\"34\">" + doc.tags + "</textarea><br>"; |
947 | - content += "<b>Description: </b><br><textarea class=\"field\" name=\"description\" rows=\"5\" cols=\"34\">" + doc.description + "</textarea><br>"; |
948 | - content += "<b>Notes: </b><br><textarea class=\"field\" name=\"notes\" rows=\"5\" cols=\"34\">" + doc.notes + "</textarea><br>"; |
949 | - content += "</form>"; |
950 | - preview.innerHTML = content; |
951 | - |
952 | - //oText = oForm.elements["tags"]; |
953 | - //document.write(oText.value); |
954 | - |
955 | - |
956 | -}; |
957 | - |
958 | -function set_data(id, tag, value){ |
959 | - return; |
960 | - for (item in dmedia.data){ |
961 | - if (dmedia.data[item]._id == id){ |
962 | - eval("data[item]." + tag + " = \"" + value + "\"") |
963 | - }; |
964 | - }; |
965 | -}; |
966 | - |
967 | -function search_for(string){ |
968 | - confirm("Really " + string + "?"); |
969 | -}; |
970 | - |
971 | -function close_box(){ |
972 | - var box = document.getElementById('info'); |
973 | - var dim = document.getElementById('dim'); |
974 | - |
975 | - box.className += " out"; |
976 | - dim.className = "out"; |
977 | -}; |
978 | - |
979 | - |
980 | -var dmedia = { |
981 | - db: new couch.Database('dmedia', '/'), |
982 | - |
983 | - data: [], |
984 | - |
985 | - load: function() { |
986 | - dmedia.browser = new Browser('browser', dmedia.db); |
987 | - dmedia.browser.run(); |
988 | - }, |
989 | -} |
990 | |
991 | === removed file 'ui/couch.js' |
992 | --- ui/couch.js 2011-11-22 10:27:54 +0000 |
993 | +++ ui/couch.js 1970-01-01 00:00:00 +0000 |
994 | @@ -1,500 +0,0 @@ |
995 | -/* |
996 | -JavaScript port of Python3 `microfiber` CouchDB adapter: |
997 | - |
998 | - https://launchpad.net/microfiber |
999 | - |
1000 | -This takes inpiration from the CoffeeScript port by Richard Lyon (aka |
1001 | -"richthegeek"): |
1002 | - |
1003 | - http://bazaar.launchpad.net/~dmedia/dmedia/trunk/view/head:/dmedia/webui/data/microfiber.coffee |
1004 | - |
1005 | -Rather than inventing an API, this is a simple adapter for calling a REST JSON |
1006 | -API like CouchDB. The goal is to make something that doesn't need constant |
1007 | -maintenances as additional features are added to the CouchDB API. |
1008 | - |
1009 | -For some good documentation of the CouchDB REST API, see: |
1010 | - |
1011 | - http://docs.couchone.com/couchdb-api/ |
1012 | - |
1013 | -Examples: |
1014 | - |
1015 | ->>> var server = new couch.Server('/'); |
1016 | ->>> server.put(null, 'mydb'); // Create database 'mydb' |
1017 | -{ok: true} |
1018 | ->>> var database = couch.Database('mydb', '/'); // One way |
1019 | ->>> var database = server.database('mydb'); // Or another, does same thing |
1020 | ->>> var doc = {foo: 'bar'}; |
1021 | ->>> database.save(doc); // POST to couch, update doc _id & _rev in place |
1022 | -{ok: true, id: '2c370303', rev: '1-7a00dff5'} |
1023 | ->>> doc |
1024 | -{_id: '2c370303', _rev: '1-7a00dff5', foo: 'bar'} |
1025 | ->>> database.post(null, '_compact'); // Compact db 'mydb' |
1026 | -{ok: true} |
1027 | ->>> server.post(null, ['mydb', '_compact']); // Same as above |
1028 | -{ok: true} |
1029 | - |
1030 | -*/ |
1031 | - |
1032 | -"use strict"; |
1033 | - |
1034 | -var couch = {}; |
1035 | - |
1036 | - |
1037 | -couch.errors = { |
1038 | - 400: 'BadRequest', |
1039 | - 401: 'Unauthorized', |
1040 | - 403: 'Forbidden', |
1041 | - 404: 'NotFound', |
1042 | - 405: 'MethodNotAllowed', |
1043 | - 406: 'NotAcceptable', |
1044 | - 409: 'Conflict', |
1045 | - 412: 'PreconditionFailed', |
1046 | - 415: 'BadContentType', |
1047 | - 416: 'BadRangeRequest', |
1048 | - 417: 'ExpectationFailed', |
1049 | -} |
1050 | - |
1051 | - |
1052 | -couch.CouchRequest = function(Request) { |
1053 | - var Request = Request || XMLHttpRequest; |
1054 | - this.req = new Request(); |
1055 | -} |
1056 | -couch.CouchRequest.prototype = { |
1057 | - |
1058 | - on_readystatechange: function() { |
1059 | - if (this.req.readyState == 4) { |
1060 | - this.callback(this); |
1061 | - } |
1062 | - }, |
1063 | - |
1064 | - request: function(callback, method, url, obj) { |
1065 | - this.callback = callback; |
1066 | - var self = this; |
1067 | - this.req.onreadystatechange = function() { |
1068 | - self.on_readystatechange(); |
1069 | - } |
1070 | - this.do_request(true, method, url, obj); |
1071 | - }, |
1072 | - |
1073 | - request_sync: function(method, url, obj) { |
1074 | - this.do_request(false, method, url, obj); |
1075 | - }, |
1076 | - |
1077 | - do_request: function(async, method, url, obj) { |
1078 | - this.req.open(method, url, async); |
1079 | - this.req.setRequestHeader('Accept', 'application/json'); |
1080 | - if (method == 'POST' || method == 'PUT') { |
1081 | - this.req.setRequestHeader('Content-Type', 'application/json'); |
1082 | - if (obj) { |
1083 | - this.req.send(JSON.stringify(obj)); |
1084 | - } |
1085 | - else { |
1086 | - this.req.send(); |
1087 | - } |
1088 | - } |
1089 | - else { |
1090 | - this.req.send(); |
1091 | - } |
1092 | - }, |
1093 | - |
1094 | - read: function() { |
1095 | - if (!this.req.status) { |
1096 | - throw 'RequestError'; |
1097 | - } |
1098 | - if (this.req.status >= 500) { |
1099 | - throw 'ServerError'; |
1100 | - } |
1101 | - if (this.req.status >= 400) { |
1102 | - var error = couch.errors[this.req.status]; |
1103 | - if (error) { |
1104 | - throw error; |
1105 | - } |
1106 | - throw 'ClientError'; |
1107 | - } |
1108 | - if (this.req.getResponseHeader('Content-Type') == 'application/json') { |
1109 | - return JSON.parse(this.req.responseText); |
1110 | - } |
1111 | - return this.req.responseText; |
1112 | - }, |
1113 | -} |
1114 | - |
1115 | - |
1116 | -couch.ChangesMonitor = function(callback, db, since) { |
1117 | - this.callback = callback; |
1118 | - this.db = db; |
1119 | - this.since = since; |
1120 | - this.monitor(); |
1121 | -} |
1122 | -couch.ChangesMonitor.prototype = { |
1123 | - monitor: function() { |
1124 | - var self = this; |
1125 | - var callback = function(r) { |
1126 | - self.on_request(r); |
1127 | - } |
1128 | - this.req = this.db.get(callback, '_changes', |
1129 | - {feed: 'longpoll', include_docs: true, since: this.since} |
1130 | - ); |
1131 | - }, |
1132 | - |
1133 | - on_request: function(req) { |
1134 | - var result = req.read(); |
1135 | - if (result.last_seq != this.since) { |
1136 | - this.since = result.last_seq; |
1137 | - this.callback(result); |
1138 | - } |
1139 | - this.monitor(); |
1140 | - }, |
1141 | -} |
1142 | - |
1143 | - |
1144 | -// microfiber.CouchBase |
1145 | -couch.CouchBase = function(url, Request) { |
1146 | - this.url = url || '/'; |
1147 | - if (this.url[this.url.length - 1] != '/') { |
1148 | - this.url = this.url + '/'; |
1149 | - } |
1150 | - this.basepath = this.url; |
1151 | - this.Request = Request || XMLHttpRequest; |
1152 | -} |
1153 | -couch.CouchBase.prototype = { |
1154 | - path: function(parts, options) { |
1155 | - /* |
1156 | - Construct a URL relative to this.basepath. |
1157 | - |
1158 | - Examples: |
1159 | - |
1160 | - >>> var inst = new couch.CouchBase('/foo/'); |
1161 | - >>> inst.path(); |
1162 | - '/foo/' |
1163 | - >>> inst.path('bar'); |
1164 | - '/foo/bar' |
1165 | - >>> inst.path(['bar', 'baz']); |
1166 | - '/foo/bar/baz' |
1167 | - >>> inst.path(['bar', 'baz'], {attachments: true}); |
1168 | - '/foo/bar/baz?attachments=true' |
1169 | - |
1170 | - */ |
1171 | - if (!parts) { |
1172 | - var url = this.basepath; |
1173 | - } |
1174 | - else if (typeof parts == 'string') { |
1175 | - var url = this.basepath + parts; |
1176 | - } |
1177 | - else { |
1178 | - var url = this.basepath + parts.join('/'); |
1179 | - } |
1180 | - if (options) { |
1181 | - var keys = []; |
1182 | - var key; |
1183 | - for (key in options) { |
1184 | - keys.push(key); |
1185 | - } |
1186 | - if (keys.length == 0) { |
1187 | - return url; |
1188 | - } |
1189 | - keys.sort(); |
1190 | - var query = []; |
1191 | - keys.forEach(function(key) { |
1192 | - if (options[key] === undefined) { |
1193 | - return; |
1194 | - } |
1195 | - if (['key', 'startkey', 'endkey'].indexOf(key) > -1) { |
1196 | - var value = JSON.stringify(options[key]); |
1197 | - } |
1198 | - else { |
1199 | - var value = options[key]; |
1200 | - } |
1201 | - query.push( |
1202 | - encodeURIComponent(key) + '=' + encodeURIComponent(value) |
1203 | - ); |
1204 | - }); |
1205 | - return url + '?' + query.join('&'); |
1206 | - } |
1207 | - return url; |
1208 | - }, |
1209 | - |
1210 | - request: function(callback, method, obj, parts, options) { |
1211 | - var url = this.path(parts, options); |
1212 | - var req = new couch.CouchRequest(this.Request); |
1213 | - req.request(callback, method, url, obj); |
1214 | - return req; |
1215 | - }, |
1216 | - |
1217 | - request_sync: function(method, obj, parts, options) { |
1218 | - var url = this.path(parts, options); |
1219 | - this.req = new couch.CouchRequest(this.Request); |
1220 | - this.req.request_sync(method, url, obj); |
1221 | - return this.req.read(); |
1222 | - }, |
1223 | - |
1224 | - put: function(callback, obj, parts, options) { |
1225 | - return this.request(callback, 'PUT', obj, parts, options); |
1226 | - }, |
1227 | - |
1228 | - put_sync: function(obj, parts, options) { |
1229 | - /* |
1230 | - Do a PUT request. |
1231 | - |
1232 | - Examples: |
1233 | - |
1234 | - var cb = new couch.CouchBase('/'); |
1235 | - cb.put(null, 'foo'); # create db /foo |
1236 | - cb.put({hello: 'world'}, ['foo', 'bar']); # create doc /foo/bar |
1237 | - cb.put({a: 1}, ['foo', 'baz'], {batch: true}); # with query option |
1238 | - |
1239 | - */ |
1240 | - return this.request_sync('PUT', obj, parts, options); |
1241 | - }, |
1242 | - |
1243 | - post: function(callback, obj, parts, options) { |
1244 | - return this.request(callback, 'POST', obj, parts, options); |
1245 | - }, |
1246 | - |
1247 | - post_sync: function(obj, parts, options) { |
1248 | - /* |
1249 | - Do a POST request. |
1250 | - |
1251 | - Examples: |
1252 | - |
1253 | - var cb = new couch.CouchBase('/'); |
1254 | - cb.post(null, ['foo', '_compact']); # compact db /foo |
1255 | - cb.post({_id: 'bar'}, 'foo'); # create doc /foo/bar |
1256 | - cb.post({_id: 'baz'}, 'foo', {batch: true}); # with query option |
1257 | - |
1258 | - */ |
1259 | - return this.request_sync('POST', obj, parts, options); |
1260 | - }, |
1261 | - |
1262 | - get: function(callback, parts, options) { |
1263 | - return this.request(callback, 'GET', null, parts, options); |
1264 | - }, |
1265 | - |
1266 | - get_sync: function(parts, options) { |
1267 | - /* |
1268 | - Do a GET request. |
1269 | - |
1270 | - Examples: |
1271 | - |
1272 | - var cb = new couch.CouchBase('/'); |
1273 | - cb.get(); # info about server |
1274 | - cb.get('foo'); # info about db /foo |
1275 | - cb.get(['foo', 'bar']); # get doc /foo/bar |
1276 | - cb.get(['foo', 'bar'], {attachments: true}); # include attachments |
1277 | - cb.get(['foo', 'bar', 'baz']); # get attachment /foo/bar/baz |
1278 | - */ |
1279 | - return this.request_sync('GET', null, parts, options); |
1280 | - }, |
1281 | - |
1282 | - delete: function(callback, parts, options) { |
1283 | - return this.request(callback, 'DELETE', null, parts, options); |
1284 | - }, |
1285 | - |
1286 | - delete_sync: function(parts, options) { |
1287 | - /* |
1288 | - Do a DELETE request. |
1289 | - |
1290 | - Examples: |
1291 | - |
1292 | - var cb = new couch.CouchBase('/'); |
1293 | - cb.delete(['foo', 'bar', 'baz'], {rev: '1-blah'}); # delete attachment |
1294 | - cb.delete(['foo', 'bar'], {rev: '2-flop'}); # delete doc |
1295 | - cb.delete('foo'); # delete database |
1296 | - */ |
1297 | - return this.request_sync('DELETE', null, parts, options); |
1298 | - }, |
1299 | -} |
1300 | - |
1301 | - |
1302 | -// microfiber.Server |
1303 | -couch.Server = function(url, Request) { |
1304 | - couch.CouchBase.call(this, url, Request); |
1305 | -} |
1306 | -couch.Server.prototype = { |
1307 | - database: function(name) { |
1308 | - /* |
1309 | - Return a new couch.Database whose base url is this.url + name. |
1310 | - */ |
1311 | - return new couch.Database(name, this.url, this.Request); |
1312 | - }, |
1313 | -} |
1314 | -couch.Server.prototype.__proto__ = couch.CouchBase.prototype; |
1315 | - |
1316 | - |
1317 | -// microfiber.Database |
1318 | -couch.Database = function(name, url, Request) { |
1319 | - /* |
1320 | - Make requests related to a database URL. |
1321 | - |
1322 | - Examples: |
1323 | - |
1324 | - >>> var db = new couch.Database('dmedia', '/'); |
1325 | - >>> db.url; |
1326 | - "/" |
1327 | - >>> db.basepath; |
1328 | - "/dmedia/" |
1329 | - >>> db.name; |
1330 | - "dmedia" |
1331 | - |
1332 | - */ |
1333 | - couch.CouchBase.call(this, url, Request); |
1334 | - this.basepath = this.url + name + '/'; |
1335 | - this.name = name; |
1336 | -} |
1337 | -couch.Database.prototype = { |
1338 | - save: function(doc) { |
1339 | - /* |
1340 | - Save *doc* to Couch, update *doc* _id and _rev in place. |
1341 | - |
1342 | - Examples: |
1343 | - |
1344 | - >>> var db = new couch.Database('mydb'); |
1345 | - >>> var doc = {foo: 'bar'}; |
1346 | - >>> db.save(doc); |
1347 | - {ok: true, id: '2c370303', rev: '1-7a00dff5'} |
1348 | - >>> doc |
1349 | - {_id: '2c370303', _rev: '1-7a00dff5', foo: 'bar'} |
1350 | - |
1351 | - */ |
1352 | - var r = this.post_sync(doc); |
1353 | - doc['_rev'] = r['rev']; |
1354 | - doc['_id'] = r['id']; |
1355 | - return r; |
1356 | - }, |
1357 | - |
1358 | - bulksave: function(docs) { |
1359 | - var rows = this.post_sync({docs: docs, all_or_nothing: true}, '_bulk_docs'); |
1360 | - var i; |
1361 | - for (i in docs) { |
1362 | - docs[i]['_rev'] = rows[i]['rev']; |
1363 | - docs[i]['_id'] = rows[i]['id']; |
1364 | - } |
1365 | - return rows; |
1366 | - }, |
1367 | - |
1368 | - view: function(callback, design, view, options) { |
1369 | - return this.get(callback, ['_design', design, '_view', view], options); |
1370 | - }, |
1371 | - |
1372 | - view_sync: function(design, view, options) { |
1373 | - /* |
1374 | - Shortcut for making a GET request to a view. |
1375 | - |
1376 | - No magic here, just saves you having to type "_design" and "_view" over |
1377 | - and over. This: |
1378 | - |
1379 | - Database.view(design, view, options); |
1380 | - |
1381 | - Is just a shortcut for: |
1382 | - |
1383 | - Database.view(['_design', design, '_view', view], options); |
1384 | - */ |
1385 | - return this.get_sync(['_design', design, '_view', view], options); |
1386 | - }, |
1387 | - |
1388 | - att_url: function(doc_or_id, name) { |
1389 | - /* |
1390 | - Return URL of a document's attachmnet. |
1391 | - |
1392 | - Examples: |
1393 | - |
1394 | - >>> var db = new couch.Database('dmedia'); |
1395 | - undefined |
1396 | - >>> db.att_url({'_id': 'foo'}, 'thumbnail'); |
1397 | - "/dmedia/foo/thumbnail" |
1398 | - >>> db.att_url('foo', 'thumbnail'); |
1399 | - "/dmedia/foo/thumbnail" |
1400 | - |
1401 | - */ |
1402 | - if (doc_or_id instanceof Object) { |
1403 | - var _id = doc_or_id['_id']; |
1404 | - } |
1405 | - else { |
1406 | - var _id = doc_or_id; |
1407 | - } |
1408 | - return this.path([_id, name]); |
1409 | - }, |
1410 | - |
1411 | - monitor_changes: function(callback, since) { |
1412 | - return new couch.ChangesMonitor(callback, this, since); |
1413 | - }, |
1414 | - |
1415 | -} |
1416 | -couch.Database.prototype.__proto__ = couch.CouchBase.prototype; |
1417 | - |
1418 | - |
1419 | -couch.Session = function(db, callback) { |
1420 | - this.db = db; |
1421 | - this.callback = callback; |
1422 | - this.docs = {}; |
1423 | - this.dirty = {}; |
1424 | - this.s = new couch.Server(this.db.url); |
1425 | - this.session_id = this.s.get('_uuids', {count: 1}).uuids[0]; |
1426 | - console.log(this.session_id); |
1427 | -} |
1428 | -couch.Session.prototype = { |
1429 | - start: function() { |
1430 | - var r = this.db.get('_all_docs', {include_docs: true}); |
1431 | - r.rows.forEach(function(row) { |
1432 | - var doc = row.doc; |
1433 | - this.docs[doc._id] = doc; |
1434 | - }, this); |
1435 | - var self = this; |
1436 | - var callback = function(r) { |
1437 | - self.on_changes(r); |
1438 | - } |
1439 | - var since = this.db.get().update_seq; |
1440 | - this.monitor = this.db.monitor_changes(callback, since); |
1441 | - }, |
1442 | - |
1443 | - on_changes: function(r) { |
1444 | - r.results.forEach(function(row) { |
1445 | - var doc = row.doc; |
1446 | - if (doc.session_id != this.session_id) { |
1447 | - this.docs[doc._id] = doc; |
1448 | - this.callback(doc); |
1449 | - } |
1450 | - }, this); |
1451 | - }, |
1452 | - |
1453 | - on_complete: function(req) { |
1454 | - this.req = null; |
1455 | - var rows = req.read(); |
1456 | - rows.forEach(function(row) { |
1457 | - this.docs[row.id]._rev = row.rev; |
1458 | - |
1459 | - }, this); |
1460 | - if (this.pending) { |
1461 | - this.pending = false; |
1462 | - this.commit(); |
1463 | - } |
1464 | - }, |
1465 | - |
1466 | - mark: function(doc) { |
1467 | - /* |
1468 | - Mark *doc* as dirty, will save when Session.commit() is called. |
1469 | - */ |
1470 | - this.dirty[doc._id] = doc; |
1471 | - doc.session_id = this.session_id; |
1472 | - }, |
1473 | - |
1474 | - commit: function() { |
1475 | - var docs = []; |
1476 | - var _id; |
1477 | - for (_id in this.dirty) { |
1478 | - docs.push(this.dirty[_id]); |
1479 | - } |
1480 | - if (docs.length == 0) { |
1481 | - return; |
1482 | - } |
1483 | - if (this.req) { |
1484 | - this.pending = true; |
1485 | - return; |
1486 | - } |
1487 | - var self = this; |
1488 | - var callback = function(req) { |
1489 | - self.on_complete(req); |
1490 | - } |
1491 | - this.req = this.db.post(callback, {docs: docs, all_or_nothing: true}, '_bulk_docs'); |
1492 | - }, |
1493 | -} |
1494 | - |
1495 | |
1496 | === modified file 'ui/index.html' |
1497 | --- ui/index.html 2011-12-17 12:15:58 +0000 |
1498 | +++ ui/index.html 2011-12-21 13:10:29 +0000 |
1499 | @@ -2,7 +2,7 @@ |
1500 | <html> |
1501 | <head> |
1502 | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
1503 | - <script src="couch.js"></script> |
1504 | + <script src="/_apps/dc3/couch.js"></script> |
1505 | <script src="/_apps/dc3/base.js"></script> |
1506 | <script src="signal.js"></script> |
1507 | <title>dmedia</title> |
1508 | @@ -10,7 +10,7 @@ |
1509 | </head> |
1510 | <body> |
1511 | <div id="header"> |
1512 | - <div class="tab active" id="import"> |
1513 | + <div class="tab" id="import"> |
1514 | Import |
1515 | </div> |
1516 | <div class="tab" id="history"> |
1517 | @@ -24,7 +24,7 @@ |
1518 | </div> |
1519 | </div> |
1520 | |
1521 | - <div id="import_target" class="content"> |
1522 | + <div id="import_target" class="content hide"> |
1523 | <div class="row"> |
1524 | <div class="grid_2"> |
1525 | <textarea placeholder="comma, seperated, tags"></textarea> |
1526 | @@ -50,7 +50,17 @@ |
1527 | <div class="row">History</div> |
1528 | </div> |
1529 | <div id="browser_target" class="content hide"> |
1530 | - <div class="row">Browser</div> |
1531 | + <video width="640" height="360" id="player"></video> |
1532 | + <p> |
1533 | + <span id="aperture"></span> |
1534 | + <span id="shutter"></span> |
1535 | + <span id="iso"></span> |
1536 | + </p> |
1537 | + <p> |
1538 | + <span id="camera"></span> |
1539 | + <span id="lens"></span> |
1540 | + <p> |
1541 | + <div id="tray"></div> |
1542 | </div> |
1543 | <div id="storage_target" class="content hide"> |
1544 | <div class="row">Storage</div> |
1545 | |
1546 | === modified file 'ui/signal.js' |
1547 | --- ui/signal.js 2011-12-17 12:15:58 +0000 |
1548 | +++ ui/signal.js 2011-12-21 13:10:29 +0000 |
1549 | @@ -1,18 +1,95 @@ |
1550 | -db = new couch.Database('dmedia'); |
1551 | +"use strict"; |
1552 | + |
1553 | +var db = new couch.Database('dmedia'); |
1554 | |
1555 | var UI = { |
1556 | - new_row: function() { |
1557 | - UI.row = $el('div', {'class': 'row'}); |
1558 | - UI.cards.appendChild(UI.row); |
1559 | - }, |
1560 | -}; |
1561 | + on_doc: function(req) { |
1562 | + var doc = req.read(); |
1563 | + var keys = ['camera', 'lens', 'aperture', 'shutter', 'iso']; |
1564 | + keys.forEach(function(key) { |
1565 | + var el = $(key); |
1566 | + if (el) { |
1567 | + el.textContent = doc.meta[key]; |
1568 | + } |
1569 | + }); |
1570 | + }, |
1571 | + |
1572 | + on_view: function(req) { |
1573 | + var rows = req.read()['rows']; |
1574 | + var tray = $('tray'); |
1575 | + rows.forEach(function(row) { |
1576 | + var id = row.id; |
1577 | + var img = $el('img', |
1578 | + { |
1579 | + id: id, |
1580 | + src: db.att_url(id, 'thumbnail'), |
1581 | + } |
1582 | + ); |
1583 | + img.onclick = function() { |
1584 | + UI.play(id); |
1585 | + } |
1586 | + tray.appendChild(img); |
1587 | + }); |
1588 | + }, |
1589 | + |
1590 | + play: function(id) { |
1591 | + if (UI.selected) { |
1592 | + UI.selected.classList.remove('selected'); |
1593 | + } |
1594 | + UI.selected = $(id); |
1595 | + UI.selected.classList.add('selected'); |
1596 | + UI.player.pause(); |
1597 | + UI.player.src = ''; |
1598 | + db.get(UI.on_doc, id); |
1599 | + UI.player.src = UI.url + id; |
1600 | + UI.player.load(); |
1601 | + UI.player.play(); |
1602 | + }, |
1603 | + |
1604 | + next: function() { |
1605 | + if (UI.selected && UI.selected.nextSibling) { |
1606 | + UI.play(UI.selected.nextSibling.id); |
1607 | + } |
1608 | + }, |
1609 | + |
1610 | + tabinit: {}, |
1611 | + |
1612 | + on_tab_changed: function(tabs, id) { |
1613 | + if (!UI.tabinit[id]) { |
1614 | + UI.tabinit[id] = true; |
1615 | + UI['init_' + id](); |
1616 | + } |
1617 | + }, |
1618 | + |
1619 | + init_import: function() { |
1620 | + console.log('init_import'); |
1621 | + }, |
1622 | + |
1623 | + init_history: function() { |
1624 | + console.log('init_history'); |
1625 | + }, |
1626 | + |
1627 | + init_browser: function() { |
1628 | + UI.player = $('player'); |
1629 | + UI.player.addEventListener('ended', UI.next); |
1630 | + db.view(UI.on_view, 'user', 'video', {reduce: false}); |
1631 | + }, |
1632 | + |
1633 | + init_storage: function() { |
1634 | + console.log('init_storage'); |
1635 | + }, |
1636 | + |
1637 | +} |
1638 | + |
1639 | |
1640 | window.onload = function() { |
1641 | UI.progressbar = new ProgressBar('progress'); |
1642 | UI.total = $('total'); |
1643 | UI.completed = $('completed'); |
1644 | UI.cards = $('cards'); |
1645 | + UI.url = db.get_sync('_local/peers')['self']; |
1646 | UI.tabs = new Tabs(); |
1647 | + UI.tabs.show_tab('import'); |
1648 | } |
1649 | |
1650 | |
1651 | @@ -97,6 +174,7 @@ |
1652 | } |
1653 | |
1654 | var elements = document.getElementsByClassName('tab'); |
1655 | + var i; |
1656 | for (i=0; i<elements.length; i++) { |
1657 | var element = elements[i]; |
1658 | element.onclick = make_handler(element); |
1659 | @@ -106,8 +184,6 @@ |
1660 | window.addEventListener('hashchange', function() { |
1661 | self.on_hashchange(); |
1662 | }); |
1663 | - |
1664 | - this.show_tab('import'); |
1665 | } |
1666 | |
1667 | Tabs.prototype = { |
1668 | @@ -127,6 +203,7 @@ |
1669 | } |
1670 | this.target = $(id + '_target'); |
1671 | this.target.classList.remove('hide'); |
1672 | + Signal.emit('tab_changed', [this, id]); |
1673 | }, |
1674 | } |
1675 | |
1676 | @@ -182,13 +259,16 @@ |
1677 | } |
1678 | |
1679 | |
1680 | +// Lazily init-tabs so startup is faster, more responsive |
1681 | +Signal.connect('tab_changed', UI.on_tab_changed); |
1682 | + |
1683 | + |
1684 | +// All the import related signals: |
1685 | Signal.connect('batch_started', |
1686 | function(batch_id) { |
1687 | $hide('summary'); |
1688 | $show('info'); |
1689 | UI.cards.textContent = ''; |
1690 | - //UI.new_row(); |
1691 | - UI.i = 0; |
1692 | UI.total.textContent = ''; |
1693 | UI.completed.textContent = ''; |
1694 | UI.progressbar.progress = 0; |
1695 | @@ -205,11 +285,6 @@ |
1696 | |
1697 | Signal.connect('import_started', |
1698 | function(basedir, import_id, info) { |
1699 | -// if (UI.i > 0 && UI.i % 4 == 0) { |
1700 | -// UI.new_row(); |
1701 | -// } |
1702 | -// UI.i += 1; |
1703 | - |
1704 | var div = $el('div', {'id': import_id, 'class': 'thumbnail'}); |
1705 | var inner = $el('div'); |
1706 | div.appendChild(inner); |
1707 | @@ -242,7 +317,6 @@ |
1708 | } |
1709 | ); |
1710 | |
1711 | - |
1712 | Signal.connect('batch_finalized', |
1713 | function(batch_id, stats, copies, msg) { |
1714 | $hide('info'); |
1715 | |
1716 | === modified file 'ui/style.css' |
1717 | --- ui/style.css 2011-12-17 12:48:01 +0000 |
1718 | +++ ui/style.css 2011-12-21 13:10:29 +0000 |
1719 | @@ -12,8 +12,12 @@ |
1720 | font-size:16px; |
1721 | line-height:1.5em; |
1722 | color:white; |
1723 | + |
1724 | background:-webkit-radial-gradient(50% 90%, 400% 100%, #51225e, #28102c); |
1725 | background-attachment:fixed; |
1726 | + /* |
1727 | + background-color: #333; |
1728 | + */ |
1729 | } |
1730 | |
1731 | .hide { |
1732 | @@ -84,10 +88,6 @@ |
1733 | padding-left: 10px; |
1734 | } |
1735 | |
1736 | -#browser_target { |
1737 | - background-color: #333; |
1738 | -} |
1739 | - |
1740 | #header { |
1741 | position: fixed; |
1742 | top: 0; |
1743 | @@ -198,3 +198,33 @@ |
1744 | .grid_4 { |
1745 | width: 920px; |
1746 | } |
1747 | + |
1748 | +#player { |
1749 | + |
1750 | +} |
1751 | + |
1752 | +#tray { |
1753 | + position: fixed; |
1754 | + right: 0; |
1755 | + top: 25px; |
1756 | + bottom: 0; |
1757 | + width: 212px; |
1758 | + background-color: #000; |
1759 | + overflow-y: scroll; |
1760 | + overflow-x: hidden; |
1761 | + -webkit-user-select: none; |
1762 | + z-index: 200; |
1763 | +} |
1764 | + |
1765 | +#tray img { |
1766 | + -webkit-user-select: none; |
1767 | + display: block; |
1768 | + border: 3px solid #000; |
1769 | + width: 192px; |
1770 | + height: 108px; |
1771 | +} |
1772 | + |
1773 | +#tray img.selected { |
1774 | + border-color: #e81f3b; |
1775 | +} |
1776 | + |
1777 | |
1778 | === removed file 'ui/test_browser.js' |
1779 | --- ui/test_browser.js 2011-04-17 22:53:45 +0000 |
1780 | +++ ui/test_browser.js 1970-01-01 00:00:00 +0000 |
1781 | @@ -1,26 +0,0 @@ |
1782 | -// Unit tests for browser.js |
1783 | - |
1784 | -"use strict"; |
1785 | - |
1786 | -py.TestBrowser = { |
1787 | - |
1788 | - test_init: function() { |
1789 | - // Retrieve by ID: |
1790 | - var db = new couch.Database('/couch/'); |
1791 | - var b = new Browser('example', db); |
1792 | - py.assertTrue(b.el instanceof Element); |
1793 | - py.assertEqual(b.el.tagName, 'DIV'); |
1794 | - py.assertEqual(b.el.id, 'example'); |
1795 | - py.assertTrue(b.db == db); |
1796 | - var el = b.el; |
1797 | - |
1798 | - // Make sure if you call with an Element, it's just reterned unchanged: |
1799 | - var b = new Browser(el, db); |
1800 | - py.assertTrue(b.el instanceof Element); |
1801 | - py.assertEqual(b.el.tagName, 'DIV'); |
1802 | - py.assertEqual(b.el.id, 'example'); |
1803 | - py.assertTrue(b.el == el); |
1804 | - py.assertTrue(b.db == db); |
1805 | - }, |
1806 | - |
1807 | -} |