degal.py
changeset 1 740133ab6353
child 5 156cdfffef8e
equal deleted inserted replaced
0:5dbdcb79024b 1:740133ab6353
       
     1 #!/usr/bin/env python2.4
       
     2 #
       
     3 # DeGAL - A pretty simple web image gallery
       
     4 # Copyright (C) 2007 Tero Marttila
       
     5 # http://marttila.de/~terom/degal/
       
     6 #
       
     7 # This program is free software; you can redistribute it and/or modify
       
     8 # it under the terms of the GNU General Public License as published by
       
     9 # the Free Software Foundation; either version 2 of the License, or
       
    10 # (at your option) any later version.
       
    11 #
       
    12 # This program is distributed in the hope that it will be useful,
       
    13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
       
    14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       
    15 # GNU General Public License for more details.
       
    16 #
       
    17 # You should have received a copy of the GNU General Public License
       
    18 # along with this program; if not, write to the
       
    19 # Free Software Foundation, Inc.,
       
    20 # 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
       
    21 #
       
    22 
       
    23 import os.path, os
       
    24 import urllib
       
    25 import logging
       
    26 import string
       
    27 from datetime import datetime
       
    28 import struct
       
    29 import base64
       
    30 import shelve
       
    31 
       
    32 import PIL
       
    33 import PIL.Image
       
    34 
       
    35 import utils
       
    36 
       
    37 __version__ = '0.2'
       
    38 
       
    39 TEMPLATE_DIR='templates'
       
    40 TEMPLATE_EXT='html'
       
    41 
       
    42 logging.basicConfig(
       
    43     level=logging.INFO,
       
    44     format="%(name)8s %(levelname)8s   %(lineno)3d %(message)s",
       
    45 
       
    46 )
       
    47 
       
    48 tpl = logging.getLogger('tpl')
       
    49 index = logging.getLogger('index')
       
    50 prepare = logging.getLogger('prepare')
       
    51 render = logging.getLogger('render')
       
    52 
       
    53 tpl.setLevel(logging.WARNING)
       
    54 
       
    55 #for l in (tpl, index, prepare, render) :
       
    56 #    l.setLevel(logging.DEBUG)
       
    57 
       
    58 def readFile (path) :
       
    59     fo = open(path, 'r')
       
    60     data = fo.read()
       
    61     fo.close()
       
    62 
       
    63     return data
       
    64 
       
    65 class Template (object) :
       
    66     GLOBALS = dict(
       
    67         VERSION=__version__,
       
    68     )
       
    69 
       
    70     def __init__ (self, name) :
       
    71         self.name = name
       
    72         self.path = os.path.join(TEMPLATE_DIR, "%s.%s" % (name, TEMPLATE_EXT))
       
    73 
       
    74         tpl.debug("Template %s at %s", name, self.path)
       
    75 
       
    76         self.content = readFile(self.path)
       
    77 
       
    78     def render (self, **vars) :
       
    79         content = self.content
       
    80         
       
    81         vars.update(self.GLOBALS)
       
    82 
       
    83         for name, value in vars.iteritems() :
       
    84             content = content.replace('<!-- %s -->' % name, str(value))
       
    85 
       
    86         return content
       
    87 
       
    88     def renderTo (self, path, **vars) :
       
    89         tpl.info("Render %s to %s", self.name, path)
       
    90 
       
    91         fo = open(path, 'w')
       
    92         fo.write(self.render(**vars))
       
    93         fo.close()
       
    94 
       
    95 gallery_tpl = Template('gallery')
       
    96 image_tpl = Template('image')
       
    97 
       
    98 IMAGE_EXTS = ('jpg', 'jpeg', 'png', 'gif', 'bmp')
       
    99 
       
   100 THUMB_DIR = 'thumbs'
       
   101 PREVIEW_DIR = 'previews'
       
   102 TITLE_FILE = 'title.txt'
       
   103 
       
   104 THUMB_GEOM = (160, 120)
       
   105 PREVIEW_GEOM = (640, 480)
       
   106 
       
   107 DEFAULT_TITLE = 'Image gallery'
       
   108 
       
   109 def isImage (fname) :
       
   110     """
       
   111         Is the given filename likely to be an image file?
       
   112     """
       
   113 
       
   114     fname = fname.lower()
       
   115     base, ext = os.path.splitext(fname)
       
   116     ext = ext.lstrip('.')
       
   117 
       
   118     return ext in IMAGE_EXTS
       
   119 
       
   120 def link (url, title) :
       
   121     """
       
   122         Returns an <a href=""></a> link tag with the given values
       
   123     """
       
   124 
       
   125     return "<a href='%s'>%s</a>" % (urllib.quote(url), title)
       
   126 
       
   127 def dirUp (count=1) :
       
   128     """
       
   129         Returns a relative path to the directly count levels above the current one
       
   130     """
       
   131 
       
   132     if not count :
       
   133         return '.'
       
   134 
       
   135     return os.path.join(*(['..']*count))
       
   136 
       
   137 def readTitleDescr (path) :
       
   138     """
       
   139         Read a title.txt or <imgname>.txt file
       
   140     """
       
   141 
       
   142     if os.path.exists(path) :
       
   143         content = readFile(path)
       
   144 
       
   145         if '---' in content :
       
   146             title, descr = content.split('---', 1)
       
   147         else :
       
   148             title, descr = content, ''
       
   149 
       
   150         return title.strip(), descr.strip()
       
   151 
       
   152     return None, None
       
   153 
       
   154 class Folder (object) :
       
   155     def __init__ (self, name='.', parent=None) :
       
   156         # the directory name
       
   157         self.name = name
       
   158 
       
   159         # our parent Folder, or None
       
   160         self.parent = parent
       
   161 
       
   162         # the path to this dir, as a relative path to the root of the image gallery, always starts with .
       
   163         if parent and name :
       
   164             self.path = parent.pathFor(name)
       
   165         else :
       
   166             self.path = name
       
   167 
       
   168         # the url-path to the index.html file
       
   169         self.html_path = self.path
       
   170         
       
   171         # dict of fname -> Folder
       
   172         self.subdirs = {}
       
   173 
       
   174         # dict of fname -> Image
       
   175         self.images = {}
       
   176         
       
   177         # our human-friendly title
       
   178         self.title = None
       
   179 
       
   180         # our long-winded description
       
   181         self.descr = ''
       
   182 
       
   183         # is this folder non-empty?
       
   184         self.alive = None
       
   185         
       
   186         # self.images.values(), but sorted by filename
       
   187         self.sorted_images = []
       
   188         
       
   189         # the ShortURL key to this dir
       
   190         self.shorturl_code = None
       
   191 
       
   192         # were we filtered out?
       
   193         self.filtered = False
       
   194    
       
   195     def pathFor (self, *fnames) :
       
   196         """
       
   197             Return a root-relative path to the given path inside this dir
       
   198         """
       
   199         return os.path.join(self.path, *fnames)
       
   200 
       
   201     def index (self, filters=None) :
       
   202         """
       
   203             Look for other dirs and images inside this dir. Filters must be either None,
       
   204             whereupon all files will be included, or a dict of {filename -> next_filter}.
       
   205             If given, only filenames that are present in the dict will be indexed, and in
       
   206             the case of dirs, the next_filter will be passed on to that Folder's index
       
   207             method.
       
   208         """
       
   209 
       
   210         index.info("Indexing %s", self.path)
       
   211 
       
   212         if filters :
       
   213             self.filtered = True
       
   214         
       
   215         # iterate through listdir
       
   216         for fname in os.listdir(self.path) :
       
   217             # the full filesystem path to it
       
   218             fpath = self.pathFor(fname)
       
   219             
       
   220             # ignore dotfiles
       
   221             if fname.startswith('.') :
       
   222                 index.debug("Skipping dotfile %s", fname)
       
   223                 continue
       
   224             
       
   225             # apply filters
       
   226             if filters :
       
   227                 if fname in filters :
       
   228                     next_filter = filters[fname]
       
   229                 else :
       
   230                     index.debug("Skip `%s' as we have a filter", fname)
       
   231                     continue
       
   232             else :
       
   233                 next_filter = None
       
   234                 
       
   235             # recurse into subdirs, but not thumbs/previews
       
   236             if os.path.isdir(fpath) and fname not in (THUMB_DIR, PREVIEW_DIR) :
       
   237                 index.debug("Found subdir %s", fpath)
       
   238                 f = self.subdirs[fname] = Folder(fname, self)
       
   239                 f.index(next_filter)   # recursion
       
   240 
       
   241             # handle images
       
   242             elif os.path.isfile(fpath) and isImage(fname) :
       
   243                 index.debug("Found image %s", fname)
       
   244                 self.images[fname] = Image(self, fname)
       
   245 
       
   246             # ignore everything else
       
   247             else :
       
   248                 index.debug("Ignoring file %s", fname)
       
   249 
       
   250     def prepare (self) :
       
   251         """
       
   252             Prepare the dir, i.e. sort+prepare the images, as well as recurse
       
   253             into subdirs
       
   254         """
       
   255 
       
   256         prepare.info("Preparing dir %s", self.path)
       
   257         
       
   258         # is this folder non-empty?
       
   259         alive = False
       
   260 
       
   261         # only create thumbs/previews dirs if we have images in here
       
   262         if self.images :
       
   263             # folder is non-empty
       
   264             alive = True
       
   265             prepare.info("Have %d images", len(self.images))
       
   266             
       
   267             # create the thumb/preview dirs if needed
       
   268             for dir in (THUMB_DIR, PREVIEW_DIR) :
       
   269                 prepare.debug("Checking for existance of %s dir", dir)
       
   270                 path = self.pathFor(dir)
       
   271 
       
   272                 if not os.path.isdir(path) :
       
   273                     prepare.info("Creating dir %s", path)
       
   274                     os.mkdir(path)
       
   275             
       
   276             # sort the images
       
   277             fnames = self.images.keys()
       
   278             fnames.sort()
       
   279             
       
   280             prev = None
       
   281             
       
   282             # link them together and prepare them
       
   283             for fname in fnames :
       
   284                 img = self.images[fname]
       
   285 
       
   286                 prepare.debug("Linking %s <-> %s", prev, img)
       
   287 
       
   288                 img.prev = prev
       
   289 
       
   290                 if prev :
       
   291                     prev.next = img
       
   292 
       
   293                 prev = img
       
   294                 
       
   295                 img.prepare()
       
   296                 
       
   297                 # add to the sorted images list
       
   298                 self.sorted_images.append(img)
       
   299         
       
   300         # prepare subdirs
       
   301         if self.subdirs :
       
   302             prepare.info("Have %d subdirs", len(self.subdirs))
       
   303             
       
   304             # just recurse into them, we're alive if one of them is
       
   305             for dir in self.subdirs.itervalues() :
       
   306                 if dir.prepare() :
       
   307                     alive = True
       
   308 
       
   309         # figure out our title
       
   310         title_path = self.pathFor(TITLE_FILE)
       
   311         
       
   312         title, descr = readTitleDescr(title_path)
       
   313 
       
   314         if title :
       
   315             prepare.info("Found title/descr")
       
   316             self.title = title
       
   317             self.descr = descr
       
   318             alive = True
       
   319         
       
   320         # default title for the root dir
       
   321         elif self.name == '.' :
       
   322             self.title = 'Index'
       
   323 
       
   324         else :
       
   325             self.title = self.name
       
   326 
       
   327         prepare.debug("Our title is '%s'", self.title)
       
   328         
       
   329         # lol ded
       
   330         if not alive :
       
   331             prepare.info("Dir %s is not alive", self.path)
       
   332 
       
   333         self.alive = alive
       
   334 
       
   335         return alive
       
   336 
       
   337     def getObjInfo (self) :
       
   338         """
       
   339             Metadata for shorturls2.db
       
   340         """
       
   341         return 'dir', self.path, ''
       
   342 
       
   343     def linkTag (self) :
       
   344         """
       
   345             A text-link to this dir
       
   346         """
       
   347 
       
   348         return link(self.path, self.title)
       
   349 
       
   350     def breadcrumb (self) :
       
   351         """
       
   352             Returns a [(fname, title)] list of this dir's parent dirs
       
   353         """
       
   354 
       
   355         f = self
       
   356         b = []
       
   357         d = 0
       
   358         
       
   359         while f :
       
   360             b.insert(0, (dirUp(d), f.title))
       
   361 
       
   362             d += 1
       
   363             f = f.parent
       
   364         
       
   365         return b
       
   366 
       
   367     def inRoot (self, *fnames) :
       
   368         """
       
   369             Return a relative URL from this dir to the given path in the root dir
       
   370         """
       
   371 
       
   372         c = len(self.path.split('/')) - 1
       
   373 
       
   374         return os.path.join(*((['..']*c) + list(fnames)))
       
   375 
       
   376     def render (self) :
       
   377         """
       
   378             Render the index.html, Images, and recurse into subdirs
       
   379         """
       
   380         
       
   381         # ded folders are skipped
       
   382         if not self.alive :
       
   383             render.info("Skipping dir %s", self.path)
       
   384             return
       
   385         
       
   386         # if this dir's contents were filtered out, then we can't render the index.html, as we aren't aware of all the images in here
       
   387         if self.filtered :
       
   388             render.warning("Dir `%s' contents were filtered, so we won't render the gallery index again", self.path)
       
   389 
       
   390         else :
       
   391             # sort the subdirs
       
   392             subdirs = self.subdirs.items()
       
   393             subdirs.sort()
       
   394             
       
   395             # generate the <a href=""></a>'s for the subdirs
       
   396             subdir_linkTags = [link(f.name, f.title) for fname, f in subdirs if f.alive]
       
   397             
       
   398             # stick them into a list
       
   399             if subdir_linkTags :
       
   400                 directories = "<ul>\n\t<li>%s</li>\n</ul>" % "</li>\n\t<li>".join(subdir_linkTags)
       
   401             else :
       
   402                 directories = ''
       
   403 
       
   404             render.info("Rendering %s", self.path)
       
   405             
       
   406             # render to index.html
       
   407             gallery_tpl.renderTo(self.pathFor('index.html'), 
       
   408                 STYLE_URL=self.inRoot('style.css'),
       
   409                 BREADCRUMB=" &raquo; ".join([link(u, t) for (u, t) in self.breadcrumb()]),
       
   410                 TITLE=self.title,
       
   411                 DIRECTORIES=directories,
       
   412                 CONTENT="".join([i.thumbImgTag() for i in self.sorted_images]),
       
   413                 DESCR=self.descr,
       
   414                 SHORTURL=self.inRoot('s', self.shorturl_code),
       
   415                 SHORTURL_CODE=self.shorturl_code,
       
   416             )
       
   417         
       
   418         # render images
       
   419         for img in self.images.itervalues() :
       
   420             img.render()
       
   421         
       
   422         # recurse into subdirs
       
   423         for dir in self.subdirs.itervalues() :
       
   424             dir.render()
       
   425                     
       
   426 class Image (object) :
       
   427     def __init__ (self, dir, name) :
       
   428         # the image filename, e.g. DSC3948.JPG
       
   429         self.name = name
       
   430 
       
   431         # the Folder object that we are in
       
   432         self.dir = dir
       
   433         
       
   434         # the relative path from the root to us
       
   435         self.path = dir.pathFor(name)
       
   436 
       
   437         # the basename+ext, e.g. DSCR3948, .JPG
       
   438         self.base_name, self.ext = os.path.splitext(name)
       
   439         
       
   440         # the root-relative paths to the thumb and preview images
       
   441         self.thumb_path = self.dir.pathFor(THUMB_DIR, self.name)
       
   442         self.preview_path = self.dir.pathFor(PREVIEW_DIR, self.name)
       
   443         
       
   444         # our user-friendly title
       
   445         self.title = name
       
   446 
       
   447         # our long-winded description
       
   448         self.descr = ''
       
   449 
       
   450         # the image before and after us, both may be None
       
   451         self.prev = self.next = None
       
   452         
       
   453         # the name of the .html gallery view thing for this image, *always* self.name + ".html"
       
   454         self.html_name = self.name + ".html"
       
   455 
       
   456         # the root-relative path to the gallery view
       
   457         self.html_path = self.dir.pathFor(self.html_name)
       
   458         
       
   459         #
       
   460         # Figured out after prepare
       
   461         #
       
   462 
       
   463         # (w, h) tuple
       
   464         self.img_size = None
       
   465         
       
   466         # the ShortURL code for this image
       
   467         self.shorturl_code = None
       
   468 
       
   469         # what to use in the rendered templates, intended to be overridden by subclasses
       
   470         self.series_act = "add"
       
   471         self.series_verb = "Add to"
       
   472     
       
   473     def prepare (self) :
       
   474         """
       
   475             Generate the thumbnail/preview views if needed, get the image info, and look for the title
       
   476         """
       
   477 
       
   478         prepare.info("Preparing image %s", self.path)
       
   479 
       
   480         # stat the image file to get the filesize and mtime
       
   481         st = os.stat(self.path)
       
   482 
       
   483         self.filesize = st.st_size
       
   484         self.timestamp = st.st_mtime
       
   485         
       
   486         # open the image in PIL to get image attributes + generate thumbnails
       
   487         img = PIL.Image.open(self.path)
       
   488 
       
   489         self.img_size = img.size
       
   490 
       
   491         for out_path, geom in ((self.thumb_path, THUMB_GEOM), (self.preview_path, PREVIEW_GEOM)) :
       
   492             # if it doesn't exist, or it's older than the image itself, generate
       
   493             if not (os.path.exists(out_path) and os.stat(out_path).st_mtime > self.timestamp) :
       
   494                 prepare.info("Create thumbnailed image at %s with geom %s", out_path, geom)
       
   495                 
       
   496                 # XXX: is this the most efficient way to do this?
       
   497                 out_img = img.copy()
       
   498                 out_img.thumbnail(geom, resample=True)
       
   499                 out_img.save(out_path)
       
   500         
       
   501         # look for the metadata file
       
   502         title_path = self.dir.pathFor(self.base_name + '.txt')
       
   503         prepare.debug("Looking for title at %s", title_path)
       
   504         
       
   505         title, descr = readTitleDescr(title_path)
       
   506 
       
   507         if title :
       
   508             self.title = title
       
   509             self.descr = descr
       
   510 
       
   511             prepare.info("Found title `%s'", self.title)
       
   512     
       
   513     def getObjInfo (self) :
       
   514         """
       
   515             Metadata for shorturl2.db
       
   516         """
       
   517         return 'img', self.dir.path, self.name
       
   518 
       
   519     def thumbImgTag (self) :
       
   520         """
       
   521             a <a><img /></a> of this image's thumbnail. Path relative to directory we are in
       
   522         """
       
   523         return link(self.html_name, "<img src='%s' alt='%s' title='%s'>" % (os.path.join(THUMB_DIR, self.name), self.descr, self.title))
       
   524 
       
   525     def previewImgTag (self) :
       
   526         """
       
   527             a <a><img /></a> of this image's preview. Path relative to directory we are in
       
   528         """
       
   529         return link(self.name, "<img src='%s' alt='%s' title='%s'>" % (os.path.join(PREVIEW_DIR, self.name), self.descr, self.title))
       
   530 
       
   531     def linkTag (self) :
       
   532         """
       
   533             a <a></a> text-link to this image
       
   534         """
       
   535         return link(self.html_name, self.title)
       
   536 
       
   537     def breadcrumb (self) :
       
   538         """
       
   539             Returns a [(fname, title)] list of this image's parents
       
   540         """
       
   541 
       
   542         f = self.dir
       
   543         b = [(self.html_name, self.title)]
       
   544         d = 0
       
   545         
       
   546         while f :
       
   547             b.insert(0, (dirUp(d), f.title))
       
   548 
       
   549             d += 1
       
   550             f = f.parent
       
   551         
       
   552         return b
       
   553 
       
   554     def render (self) :
       
   555         """
       
   556             Write out the .html file
       
   557         """
       
   558 
       
   559         render.info("Rendering image %s", self.path)
       
   560 
       
   561         image_tpl.renderTo(self.html_path,
       
   562             STYLE_URL=self.dir.inRoot('style.css'),
       
   563             UP_URL=('.'),
       
   564             PREV_URL=(self.prev and self.prev.html_name or ''),
       
   565             NEXT_URL=(self.next and self.next.html_name or ''),
       
   566             FILE=self.name,
       
   567             BREADCRUMB=" &raquo; ".join([link(u, t) for u, t in self.breadcrumb()]),
       
   568             TITLE=self.title,
       
   569             PREVIOUS_THUMB=(self.prev and self.prev.thumbImgTag() or ''),
       
   570             IMAGE=self.previewImgTag(),
       
   571             NEXT_THUMB=(self.next and self.next.thumbImgTag() or ''),
       
   572             DESCRIPTION=self.descr,
       
   573             IMGSIZE="%dx%d" % self.img_size,
       
   574             FILESIZE=fmtFilesize(self.filesize),
       
   575             TIMESTAMP=fmtTimestamp(self.timestamp),
       
   576             SHORTURL=self.dir.inRoot('s', self.shorturl_code),
       
   577             SHORTURL_CODE=self.shorturl_code,
       
   578             SERIES_URL=self.dir.inRoot('series/%s/%s' % (self.series_act, self.shorturl_code)),
       
   579             SERIES_VERB=self.series_verb,
       
   580         )   
       
   581     
       
   582     def __str__ (self) :
       
   583         return "Image `%s' in `%s'" % (self.name, self.dir.path)
       
   584 
       
   585 def int2key (id) :
       
   586     """
       
   587         Turn an integer into a short-as-possible url-safe string
       
   588     """
       
   589     for type in ('B', 'H', 'I') :
       
   590         try :
       
   591             return base64.b64encode(struct.pack(type, id), '-_').rstrip('=')
       
   592         except struct.error :
       
   593             continue
       
   594 
       
   595     raise Exception("ID overflow: %s" % id)
       
   596 
       
   597 def updateShorturlDb (root) :
       
   598     """
       
   599         DeGAL <= 0.2 used a simple key => path mapping, but now we use
       
   600         something more structured, key => (type, dirpath, fname), where
       
   601 
       
   602         type    - one of 'img', 'dir'
       
   603         dirpath - the path to the directory, e.g. '.', './foobar', './foobar/quux'
       
   604         fname   - the filename, one of '', 'DSC9839.JPG', 'this.png', etc.
       
   605     """
       
   606 
       
   607     db = shelve.open('shorturls2', 'c')
       
   608     
       
   609     id = db.get('_id', 1)
       
   610 
       
   611     dirqueue = [root]
       
   612 
       
   613     # dict of path -> obj
       
   614     paths = {}
       
   615 
       
   616     while dirqueue :
       
   617         dir = dirqueue.pop(0)
       
   618 
       
   619         dirqueue.extend(dir.subdirs.itervalues())
       
   620 
       
   621         if dir.alive :
       
   622             paths[dir.path] = dir
       
   623 
       
   624         for img in dir.images.itervalues() :
       
   625             paths[img.html_path] = img
       
   626 
       
   627     for key in db.keys() :
       
   628         if key.startswith('_') :
       
   629             continue
       
   630 
       
   631         type, dirpath, fname = db[key]
       
   632         
       
   633         path = os.path.join(dirpath, fname)
       
   634 
       
   635         try :
       
   636             paths.pop(path).shorturl_code = key
       
   637             index.debug("Code for `%s' is %s", path, key)
       
   638 
       
   639         except KeyError :
       
   640             index.debug("Path `%s' in DB does not exist?", path)
       
   641 
       
   642     for obj in paths.itervalues() :
       
   643         key = int2key(id)
       
   644         id += 1
       
   645         
       
   646         index.info("Alloc code `%s' for `%s'", key, obj.html_path)
       
   647 
       
   648         obj.shorturl_code = key
       
   649 
       
   650         db[key] = obj.getObjInfo()
       
   651 
       
   652     db['_id'] = id
       
   653     db.close()
       
   654 
       
   655 def main (targets=()) :
       
   656     root_filter = {}
       
   657 
       
   658     for target in targets :
       
   659         f = root_filter
       
   660         for path_part in target.split('/') :
       
   661             if path_part :
       
   662                 if path_part not in f :
       
   663                     f[path_part] = {}
       
   664                 f = f[path_part]
       
   665     
       
   666     index.debug('Filter: %s', root_filter)
       
   667 
       
   668     root = Folder()
       
   669     root.index(root_filter)
       
   670     root.prepare()
       
   671     updateShorturlDb(root)
       
   672     root.render()
       
   673 
       
   674 def fmtFilesize (size) :
       
   675     return utils.formatbytes(size, forcekb=False, largestonly=True, kiloname='K', meganame='M', bytename='B', nospace=True)
       
   676 
       
   677 def fmtTimestamp (ts) :
       
   678     return datetime.fromtimestamp(ts).strftime("%Y/%m/%d %H:%M")
       
   679 
       
   680 if __name__ == '__main__' :
       
   681     from sys import argv
       
   682     argv.pop(0)
       
   683 
       
   684     main(argv)
       
   685