Merge lp:~jderose/dmedia/browser into lp:dmedia

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
Reviewer Review Type Date Requested Status
James Raymond Approve
Review via email: mp+86561@code.launchpad.net

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-}

Subscribers

People subscribed via source and target branches