* python 2.6+
  * tornado
  * jinja2
+ * PIL
  * motion
  * ffmpeg
  * v4l2-utils
 
 On a debian-based system you could run:
 
-    apt-get install python-tornado python-jinja2 motion v4l2-utils
+    apt-get install python-tornado python-jinja2 python-imaging motion v4l2-utils
 
 ## Browser Compatibility ##
 
 
+-> test IE
 -> layout seems to be too wide for a modern mobile phone
+-> add a mgmt command for generating thumbnails
+-> implement media files paging - find a solution for the many files
 -> make camera frames positions configurable
 -> add a view log functionality
 
 
         '@enabled': ui.get('enabled', False),
         '@proto': ui.get('proto', 'v4l2'),
         'videodevice': ui.get('device', ''),
-        'lightswitch': int(ui.get('light_switch_detect', False)) * 5,
+        'lightswitch': int(ui.get('light_switch_detect', True)) * 5,
         'auto_brightness': ui.get('auto_brightness', False),
         'width': int(ui['resolution'].split('x')[0]),
         'height': int(ui['resolution'].split('x')[1]),
     data.setdefault('@enabled', False)
     data.setdefault('@proto', 'v4l2')
     data.setdefault('videodevice', '/dev/video0')
-    data.setdefault('lightswitch', 0)
+    data.setdefault('lightswitch', 5)
     data.setdefault('auto_brightness', False)
     data.setdefault('brightness', 0)
     data.setdefault('contrast', 0)
 
                     camera_config.get('@username'),
                     camera_config.get('@password'),
                     camera_config.get('@remote_camera_id'), on_response,
-                    media_type='picture')
+                    media_type='picture',
+                    prefix=self.get_argument('prefix', None),
+                    stat=self.get_argument('stat', None))
         
         else:
-            pictures = mediafiles.list_media(camera_config, media_type='picture')
+            pictures = mediafiles.list_media(camera_config, media_type='picture',
+                    prefix=self.get_argument('prefix', None),
+                    stat=self.get_argument('stat', None))
             
             self.finish_json({
                 'mediaList': pictures,
                     camera_config.get('@username'),
                     camera_config.get('@password'),
                     camera_config.get('@remote_camera_id'), on_response,
-                    media_type='movie')
+                    media_type='movie',
+                    prefix=self.get_argument('prefix', None),
+                    stat=self.get_argument('stat', None))
         
         else:
-            movies = mediafiles.list_media(camera_config, media_type='movie')
+            movies = mediafiles.list_media(camera_config, media_type='movie',
+                    prefix=self.get_argument('prefix', None),
+                    stat=self.get_argument('stat', None))
             
             self.finish_json({
                 'mediaList': movies,
 
 from PIL import Image
 
 import config
+import utils
 
 
 _PICTURE_EXTS = ['.jpg']
 _MOVIE_EXTS = ['.avi', '.mp4']
 
 
-def _list_media_files(dir, exts):
+def _list_media_files(dir, exts, prefix=None):
     full_paths = []
-    for root, dirs, files in os.walk(dir):  # @UnusedVariable
-        for name in files:
+    
+    if prefix is not None:
+        if prefix == 'ungrouped':
+            prefix = ''
+        
+        root = os.path.join(dir, prefix)
+        for name in os.listdir(root):
             full_path = os.path.join(root, name)
             if not os.path.isfile(full_path):
                 continue
             
             full_paths.append(full_path)
     
+    else:    
+        for root, dirs, files in os.walk(dir):  # @UnusedVariable
+            for name in files:
+                full_path = os.path.join(root, name)
+                if not os.path.isfile(full_path):
+                    continue
+                 
+                full_path_lower = full_path.lower()
+                if not [e for e in exts if full_path_lower.endswith(e)]:
+                    continue
+                
+                full_paths.append(full_path)
+    
     return full_paths
 
 
             logging.debug('all movies have preview')
             
 
-def list_media(camera_config, media_type):
+def list_media(camera_config, media_type, prefix=None, stat=False):
     target_dir = camera_config.get('target_dir')
 
     if media_type == 'picture':
     elif media_type == 'movie':
         exts = _MOVIE_EXTS
         
-    full_paths = _list_media_files(target_dir, exts=exts)
+    full_paths = _list_media_files(target_dir, exts=exts, prefix=prefix)
     media_files = []
     
     for p in full_paths:
         path = p[len(target_dir):]
         if not path.startswith('/'):
             path = '/' + path
-            
-#         try:
-#             stat = os.stat(p)
-#         
-#         except Exception as e:
-#             logging.error('stat call failed for file %(path)s: %(msg)s' % {
-#                     'path': path, 'msg': unicode(e)})
-#             
-#             continue
-#         
-#         timestamp = stat.st_mtime
-#         size = stat.st_size
+
+        timestamp = None
+        size = None
+        
+        if stat:
+            try:
+                stat = os.stat(p)
+             
+            except Exception as e:
+                logging.error('stat call failed for file %(path)s: %(msg)s' % {
+                        'path': path, 'msg': unicode(e)})
+                 
+                continue
+ 
+            timestamp = stat.st_mtime
+            size = stat.st_size
         
         media_files.append({
             'path': path,
-            #'momentStr': utils.pretty_date_time(datetime.datetime.fromtimestamp(timestamp)),
-            #'sizeStr': utils.pretty_size(size),
-            #'timestamp': timestamp
+            'momentStr': timestamp and utils.pretty_date_time(datetime.datetime.fromtimestamp(timestamp)),
+            'sizeStr': size and utils.pretty_size(size),
+            'timestamp': timestamp
         })
     
     # TODO files listed here may not belong to the given camera
 
     http_client.fetch(request, on_response)
 
 
-def list_media(host, port, username, password, camera_id, callback, media_type):
+def list_media(host, port, username, password, camera_id, callback, media_type, prefix=None, stat=False):
     logging.debug('getting media list for remote camera %(id)s on %(host)s:%(port)s' % {
             'id': camera_id,
             'host': host,
             'port': port})
     
+    query = {}
+    if prefix is not None:
+        query['prefix'] = prefix
+    
+    if stat:
+        query['stat'] = 'true'
+    
     request = _make_request(host, port, username, password, '/%(media_type)s/%(id)s/list/' % {
-            'id': camera_id, 'media_type': media_type})
+            'id': camera_id, 'media_type': media_type}, query=query)
     
     def on_response(response):
         if response.error:
             'host': host,
             'port': port})
     
-    uri = '/%(media_type)s/%(id)s/download/%(filename)s?' % {
+    uri = '/%(media_type)s/%(id)s/download/%(filename)s' % {
             'media_type': media_type,
             'id': camera_id,
             'filename': filename}
             'host': host,
             'port': port})
     
-    uri = '/%(media_type)s/%(id)s/preview/%(filename)s?' % {
+    uri = '/%(media_type)s/%(id)s/preview/%(filename)s' % {
             'media_type': media_type,
             'id': camera_id,
             'filename': filename}
     
+    query = {}
+    
     if width:
-        uri += 'width=' + str(width)
+        query['width'] = str(width)
         
     if height:
-        uri += 'height=' + str(height)
+        query['height'] = str(height)
     
-    request = _make_request(host, port, username, password, uri)
+    request = _make_request(host, port, username, password, uri, query=query)
     
     def on_response(response):
         if response.error:
 
     padding: 1em 0px 0.2em 0px;
 }
 
+img.media-list-progress {
+    position: relative;
+    top: 35%;
+    display: block;
+    margin: auto;
+}
+
 div.media-list-entry {
     height: 4em;
     background-color: #414141;
 
                 'path': path,
                 'group': key,
                 'name': parts[parts.length - 1],
-                'cameraId': cameraId,
-                'momentStr': media.momentStr,
-                'sizeStr': media.sizeStr,
-                'timestamp': media.timestamp
+                'cameraId': cameraId
             });
         });
         
                 var nameDiv = $('<div class="media-list-entry-name">' + entry.name + '</div>');
                 entryDiv.append(nameDiv);
                 
-                if (entry.momentStr && entry.sizeStr) {
-                    var detailsDiv = $('<div class="media-list-entry-details"></div>');
-                    detailsDiv.html(entry.momentStr + ' | ' + entry.sizeStr);
-                    entryDiv.append(detailsDiv);
-                }
-                else {
-                    nameDiv.css('line-height', '2.3em');
-                }
-                
                 downloadButton[0]._onClick = function () {
                     window.location.href = '/picture/' + cameraId + '/download' + entry.path;
                     
         });
         
         function showGroup(key) {
-            groupsDiv.find('div.media-dialog-group-button').each(function () {
-                var $this = $(this);
-                if (this.key == key) {
-                    $this.addClass('current');
-                }
-                else {
-                    $this.removeClass('current');
-                }
-            });
+            if (mediaListDiv.find('img.media-list-progress').length) {
+                return; /* already in progress of loading */
+            }
             
-            mediaListDiv.html('');
-
-            var entries = groups[key];
-            entries.forEach(function (entry) {
-                mediaListDiv.append(entry.div);
-                entry.div.click(entry.div[0]._onClick);
-                
-                var downloadButton = entry.div.find('div.media-list-download-button');
-                downloadButton.click(downloadButton[0]._onClick);
-            });
+            var previewImg = $('<img class="media-list-progress" src="' + staticUrl + 'img/modal-progress.gif"/>');
+            mediaListDiv.append(previewImg);
             
-            setTimeout(function () {
-                mediaListDiv.find('img.media-list-preview').each(function () {
-                    if (this._src) {
-                        this.src = this._src;
+            var url = '/' + mediaType + '/' + cameraId + '/list/?prefix=' + (key || 'ungrouped')+ '&stat=true';
+            ajax('GET', url, null, function (data) {
+                if (data == null || data.error) {
+                    hideModalDialog();
+                    showErrorMessage(data && data.error);
+                    return;
+                }
+                
+                var entries = groups[key];
+                
+                /* index the media list by name */
+                var mediaListByName = {};
+                data.mediaList.forEach(function (media) {
+                    var path = media.path;
+                    var parts = path.split('/');
+                    var name = parts[parts.length - 1];
+                    
+                    mediaListByName[name] = media;
+                });
+                
+                /* (re)set the current state of the group buttons */
+                groupsDiv.find('div.media-dialog-group-button').each(function () {
+                    var $this = $(this);
+                    if (this.key == key) {
+                        $this.addClass('current');
+                    }
+                    else {
+                        $this.removeClass('current');
                     }
+                });
+ 
+                /* add the entries to the media list */
+                mediaListDiv.children('div.media-list-entry').detach();
+                mediaListDiv.html('');
+                entries.forEach(function (entry) {
+                    var entryDiv = entry.div;
+                    var nameDiv = entryDiv.find('div.media-list-entry-name');
+                    var detailsDiv = entryDiv.find('div.media-list-entry-details');
+                    var downloadButton = entryDiv.find('div.media-list-download-button');
                     
-                    delete this._src;
+                    var media = mediaListByName[entry.name];
+                    if (media) { /* if details are available, show them */
+                        if (detailsDiv.length === 0) {
+                            detailsDiv = $('<div class="media-list-entry-details"></div>');
+                            entryDiv.append(detailsDiv);
+                        }
+                        
+                        detailsDiv.html(media.momentStr + ' | ' + media.sizeStr);
+                    }
+                    else {
+                        nameDiv.css('line-height', '2.3em');
+                    }
+                    
+                    entryDiv.click(entryDiv[0]._onClick);
+                    downloadButton.click(downloadButton[0]._onClick);
+
+                    mediaListDiv.append(entryDiv);
                 });
-            }, 1000);
+                
+                setTimeout(function () {
+                    mediaListDiv.find('img.media-list-preview').each(function () {
+                        if (this._src) {
+                            this.src = this._src;
+                        }
+                        
+                        delete this._src;
+                    });
+                }, 1000);
+            });
         }
         
         if (keys.length) {
 
         });
         
         children.css('display', 'none');
+        updateModalDialogPosition();
         
         container[0]._onClose = onClose; /* set the new onClose handler */
         container.append(content);