Commits

fab31 committed a76b597 Merge

switch back to default branch

  • Participants
  • Parent commits 932d592, 9e3e1c2
  • Tags 0.9

Comments (0)

Files changed (72)

File .hgignore

-syntax: glob
-*.orig
-*.pyo
-*.swp
-*.pyc
-*.swo

File .hgtags

-424523ec6a7b1b993ad69ca8d1aca5bbf360fedb STABLE
-173f3ce450dc0504014e9eee5dd591ea92413458 STABLE
-072c8492eab414f797746721b8ea1767be40854d 0.6
-2d2f07051be7be779c2741a908d3274f45b536d6 STABLE
-e1cc384ebf2c923e8f6abc63023ad750e822c32c 0.7
-7696ae37c1b3b356874aa24cced442c2da4c4786 STABLE
-c06371e3c6859670b2a8968b8a14fde94fb96b3e 0.8-alpha
-52fe86c4c15b4d1232d120842e5199a1e9cebc45 0.8-beta
-41f25e30ec4fb0d80d97d48220562f90f6701e00 0.8-rc1

File NOTES

-TODO (urgent):
- Cancel downloads on select.
- Top & Bottom border of commands should be red/blue
- Left & Right divs should be stylised (background/no border) 
- Form should fade-out when user validate search,
-   a click on the bee makes it fade in
- Right align the "Search" button
- Left align the "search" form on the /search/ server
- Change font
-
-BUGS:
- Should adapt samplerate to current song (time is bended now)
- Pause is messed-up if NEXT or PREV is pressed (should un-pause)
- Check why m4a & wma support is variable (ffmpeg options ?)
-
-HIGH PRIO:
- Manage some configuration (list of hosts etc...)
- Manage status icon/menu http://www.pygtk.org/docs/pygtk/class-gtkstatusicon.html
- Rip avbin.py to something similar in aspect but much simpler
-
- Add Hide icon to hide top bar and playlist (near shuffle ? move shuffle to uri bar ?)
- Refactor player ! (it's a mess...)
- Add Generic Pack/Unpack (tar, zip...) facilities and enhance get cmd
-
-LOW PRIO:
- Add tooltip for shuffle button
-
- Add playlist management to the view
-  - inject (local file or directory)
-  - delete
-  - drag & drop
- Add a Delete button under playlist (should hide with playlist)
- Add adaptative chunk size (**2 until it takes >0.05s) to server & get cmd
-
- UI:
-  Review alignments ?
-  Contextual menu in list (with delete etc...)
-
-FUTURE:
- Add tarball/zip download to http interface
-
- Separate artist db:
-  - artist view possible
-  - difflib matching on insert
-  much slower ?? (to test...)
-
- Tags edition ? add pure "tags" + ratings (user defined)
-
- Add shoutcast/jamendo/lastfm support
- Add player in server (integrate in player ? only web ui ?)
- MTP support ? https://launchpad.net/pylibmtp
- uPnP/DLNA support ? https://coherence.beebits.net/
- daap support ? http://jerakeen.org/code/PythonDaap/
-
-LINKS:
-http://www.sacredchao.net/quodlibet/wiki/Development/Mutagen/Tutorial
-http://www.pygtk.org/docs/pygtk/index.html
-
-interesting modules:
-http://pypi.python.org/pypi/validate/0.3.1 (configuration)
-http://pypi.python.org/pypi/magicdate/0.1.2 (date lookup)
-http://code.google.com/p/pyffmpeg/ (decoding backend)
-http://svn.xiph.org/trunk/ao-python/ (playback)
- http://www.vorbis.com/files/1.0.1/unix/py/pyao-0.81.tar.gz
-http://www.luga.de/pytone/ (player)
-http://pyglet.org/ (gfx + sound backend)
-http://code.google.com/p/avbin/ (easy ffmpeg abstraction)

File README

-
-QUICKSTART
-
-# start the daemon
-./run.py serve &
-
-# read some help (to start, you only need to know "scan" command)
-./run.py help
-./run.py scan $HOME/Music/
-./run.py search artist: black or title: guitar
-
-... That' all !
-You may want to visit http://127.0.0.1:9090/ to access the web interface
-
-NOTE: if you installed zicbee system-wide,
-   you may use "zicdb" from any directory instead of using "run.py".
-   Also check "zicserve" as an alias to "zicdb serve".
-

File TODO

-=== IDEAS
-  * video support
-  * picture support
-      ** (merge with imgplayer.js ?)
-  * add update support from syntax
-    ** playlist: store a playlist (TODO: without other argument it GETS a playlist...)
-    ** +score: change the score of the given pattern
-    ** +tags: add the given keyword(s) to tags
-    ** example: `artist: Rolling album: stone +tags: rock`
-  * separate zicdb into zicdb (database/local maintenance) / zicbee (player/remote control)
-  * re-write zicbee in C (the executable, not the project !)
-    ** have python fallback (use code generation from cmd/requests description?)
-  * let choice of multiples players
-  * database operations (difference, merge, substract, etc...)
-  * allow alternative databases synchronization (SQL, XML, ...)
-  * unify output format (re-architecture output operations)
-    ** add support for rss/rdf/atom formats as output
-  * add file transcoder functions (on a given database)
-    ** could be available via text and www
-  * add more formats to the player on www "live" (flash ? :( )
-  * add "size" lookup option, limiting the size of the answer (in s)
-    ** add advanced "skip" option, to skip a number of entries
-    ** add advanced "limit" option, to limit the number of entries answered
-  * add views support (aka "dynamic playlists", stored also...)
-    ** example: `artist: Rolling album: stone +view: stones` (still could use "+playlist" to store regular playlists)
-  * pluggable audio backend/minimal plugin support:
-      ** upnp/dlna support
-      ** audioscrobbler
-        *** syntax proposition: just "audioscrobbler:" ?
-      ** podcasts (atom/rss/etc...)
-        *** example: ```feed: http://blah title: something```
-        *** add named feeds support (use "myradio" instead of http://....)
-      ** specific hardware support (ipod...)  <-- give me one if you want it ;P
-      ** more...
-  * automatic (but clever...) autodetection of mountpoints
-
-----
-
-For **1.1**: (architecture cleanup)
- * write some backends detection and loading
- * need to split zicdb executable ?
- * Much more doc
- 
-
-Before **1.0**: (very useable, db schema freeze)
- * distribution cleanup
- * fill feature gap between www (poor) & text (rich) ui
- * add themes support for www player (and DB ?)
- * good duplicates detection
-
-Before **0.9**:
- * cleaner javascript/cookies/sessions (prepare theme support)
- * improve shell completion ?
- * satisfying duplicates detection
- * rating/tag fully working (intensive tests on distributed configurations)
- * stored playlists (including position)
-    ** inc. playlist resume
-    ** www and text interface
-
-Before **0.8**: (slight feature/stable)
-<--- **WE ARE HERE**
- DONE:
- * add support for FLAC
- * interactive shell support with completion and history
-   ** see "zicdb shell" or "zicbee" commands
- * integrate/complete tagging & scoring support
- * add support for multiple DBs at once
-   ** (ie. have separate databases for your mp3 player & your local drive)
-   ** see "use" command for usage
- * complete admin commands (see "set" command)
- * better documentation
-
-
-**0.7** (cleanup/refactor):
-
- * add play, pause, next, prev, list
- * add cleaner configuration:: more unified (prepare themes handling)
-   ** ensure default host is well given
- * public repositories (Hg) setup
- * add some docs on the website
- * add screenshots on website
-
-**0.7**-rc1 (first public release)
- * site launch
- * fixes egg/root installation (temporary file created)
-
-----
-
-**the underground** (unreleased mode, use mercurial to recover)
- RIP:
-  * gtk player (many flavors tested)
-  * broken gstreamer backend (player)
-  * avbin (with pyglet) backend (player)
-  * different mplayer wrapper
-  * other flavors of www interface
-  * architectural mistakes ;)
-

File aliases

-fab@gnux.info fab
-fdevaux@wyplay.com fab
-fab@localhost.localzone.fr fab
-gustave@chenapan bergamote
-bergamote@karmacoma.org bergamote
-bergamote@bender bergamote
-sbres@sbres.static.wyplay.int seb

File docs/bensh.py

-#def fill():
-#    inp = []
-#    for name in 'A B C D E F G H'.split():
-#        for val in xrange(10):
-#            inp.append('%s%d'%(name, val))
-#    return inp
-
-def test1(test_fn):
-    try:
-        for n in xrange(10000):
-            test_fn('A%d'%n)
-    except Exception, e:
-        print "E:", e
-
-def test2(test_fn, nb):
-    cumul = []
-    for n in xrange(10000):
-        cumul.append(n)
-        if n>0 and n%nb == 0:
-            test_fn('[%s]'%(','.join('A%d'%n for n in cumul)))
-            print len(cumul)
-            cumul = []
-    if cumul:
-        test_fn('[%s]'%(','.join('A%d'%n for n in cumul)))
-
-def test3(test_fn, nb):
-    result = []
-    cumul = []
-    for n in xrange(10000):
-        cumul.append(n)
-        if n>0 and n%nb == 0:
-            res = test_fn('[%s]'%(','.join('A%d'%n for n in cumul)))
-            result.extend(res)
-            cumul = []
-    if cumul:
-        res = test_fn('[%s]'%(','.join('A%d'%n for n in cumul)))
-        result.extend(res)
-
-    return result
-
-def test4(test_fn, nb):
-    result = []
-    cumul = []
-    for n in xrange(10000):
-        cumul.append(n)
-        if n>0 and n%nb == 0:
-            res = test_fn(['A%d'%n for n in cumul])
-            result.extend(res)
-            cumul = []
-    if cumul:
-        res = test_fn('[%s]'%(','.join('A%d'%n for n in cumul)))
-        result.extend(res)
-
-    return result
-
-from simplejson import dumps as simplejson
-from cjson import encode as cjson
-from demjson import encode as demjson
-
-from itertools import count
-
-if __name__ == '__main__':
-    modules = ('cjson', 'simplejson', 'demjson')
-    from timeit import Timer
-    def _get_best(name, mod_name, modules, num, best_val, best_time):
-        if isinstance(num, str):
-            cmd = '%s(%s)'%(name, mod_name)
-        else:
-            cmd = '%s(%s, %s)'%(name, mod_name, repr(num))
-        t = Timer(cmd,
-                'from bensh import %s, %s'%(name, ', '.join(modules)))
-        tnum = t.timeit(100)
-        if best_val is None:
-            best_val = num
-            best_time = tnum
-        elif best_time > tnum:
-            best_time = tnum
-            best_val = num
-        print "%s = %.2fs"%('%%%d'%num if isinstance(num, int) else num, tnum)
-        return best_val, best_time
-
-    def _full_bensh(module_name):
-        print module_name
-
-        print "method1 (direct):"
-        best_val = None
-        best_time = None
-        _get_best('test1',
-                module_name, modules,
-                'direct',
-                best_val, best_time)
-
-#        print "method2:"
-#        best_val = None
-#        best_time = None
-#        for num in xrange(2, 1000, 50):
-#            best_val, best_time = _get_best('test2',
-#                    module_name, modules,
-#                    num,
-#                    best_val, best_time)
-#        print "the BEST VAL is for N=%s"%best_val
-        chunk_sizes = (1, 2, 10, 50, 100)
-
-        print "method3:"
-        best_val = None
-        best_time = None
-        for num in chunk_sizes:
-            best_val, best_time = _get_best('test3',
-                    module_name, modules,
-                    num,
-                    best_val, best_time)
-        print "the BEST VAL is for N=%s"%best_val
-
-        print "method4:"
-        best_val = None
-        best_time = None
-        for num in chunk_sizes:
-            best_val, best_time = _get_best('test3',
-                    module_name, modules,
-                    num,
-                    best_val, best_time)
-        print "the BEST VAL is for N=%s"%best_val
-
-    for mod_name in modules:
-        _full_bensh(mod_name)
-        print "-*-"
-
-

File docs/gtk_contextual.py

-import gtk
-
-RIGHT_CLIC = 3
-
-class Contextual(gtk.Menu):
-
-    RIGHT_CLIC = 3
-
-    def __init__(self, items_list):
-        gtk.Menu.__init__(self)
-        for it in items_list:
-            item = gtk.MenuItem(it)
-            self.append(item)
-            item.connect("activate", self.connector, it)
-            item.show()
-        self.show()
-
-    def connector(self, widget, string):
-        print string
-
-def box_event(widget, event):
-    if event.type == gtk.gdk.BUTTON_PRESS:
-        if event.button == RIGHT_CLIC:
-            widget.popup(parent_menu_shell=None, parent_menu_item=None, func=None, button=event.button, activate_time=event.time, data=None)
-            return True
-    return False
-
-if __name__ == '__main__':
-    w = gtk.Window(gtk.WINDOW_TOPLEVEL)
-    w.set_size_request(200, 200)
-    w.connect("delete_event", lambda x,e : gtk.main_quit())
-    hbox = gtk.HBox()
-    w.add(hbox)
-    menu = Contextual(["1", "2", "3"])
-    bouton = gtk.Button("TEST")
-    bouton.connect_object("event", box_event, menu)
-    hbox.pack_end(bouton, True, True, 2)
-    bouton.show()
-    hbox.show()
-    w.show()
-    gtk.main()
-
-

File eggs/buzhug-1.5-py2.5.egg

Binary file removed.

File eggs/mutagen-1.14-py2.5.egg

Binary file removed.

File eggs/python-cjson-1.0.5.tar.gz

Binary file removed.

File eggs/simplejson-2.0.4-py2.5.egg

Binary file removed.

File eggs/web.py-0.31-py2.5.egg

Binary file removed.

File run.py

-#!/usr/bin/env python
-# -*- encoding: utf-8 -*-
-
-import os
-import sys
-
-for name in os.listdir('eggs'):
-    egg_name = os.path.join(os.curdir, 'eggs', name)
-    if name.endswith('.egg') or os.path.isdir(egg_name):
-        print "appending %s to your python path..."%egg_name
-        sys.path.append(egg_name)
-
-from zicbee.core import startup
-startup()
+[install]
+optimize=1
+
+#[egg_info]
+#tag-build=-beta
+#tag-date=true
 	use_setuptools()
 from setuptools import setup, find_packages
 
-VERSION='0.8-rc1'
-
-if 'install' in sys.argv:
-    print """Warning:
-You will need to install some parts manually:
-
-If it can't build, try to comment the line:
-            'python-cjson>=1.0.5',
-
-and uncomment one of those (recommended alternative: simplejson):
-#            'simplejson>=1.7.3',
-#            'demjson>=1.1',
-
-Good luck !"""
-
-
-
-# also supported:
-#            'simplejson>=1.7.3',
-
-requirements = [ 'buzhug>=1.5', 'mutagen>=1.14' ]
-
-if sys.version_info[:2] < (2, 6):
-    # add cjson dependency
-    if os.name in ('nt', 'ce'):
-        requirements.append( 'demjson>=1.1' )
-    else:
-        requirements.append( 'python-cjson>=1.0.5' )
+sys.path.insert(0, '.')
+import zicbee_mplayer
+VERSION=zicbee_mplayer.__version__
 
 setup (
-        name='zicbee',
+        name='zicbee-mplayer',
         version=VERSION,
         author='Fabien Devaux',
         author_email='fdev31@gmail.com',
+        url = 'http://zicbee.gnux.info/',
+        download_url='http://zicbee.gnux.info/hg/index.cgi/zicbee-mplayer/archive/%s.tar.bz2'%VERSION,
         license='BSD',
-        platform='All',
-        description='A simple & powerful distributed Music database engine',
+        platform='all',
+        description='MPlayer backend for zicbee project',
         long_description='''
-ZicBee is a project grouping multiple applications to manage play and handle music databases.
-It takes ideas from Quodlibet and Mpd, both very good music mplayers with their own strengths.
-
-For now there is a Swiss-army knife tool: zicdb
-
-Some plugins for quodlibet has also be developed. ZicBee is fast,
-portable (but not very ported...) and flexible.
-
-While the project is stable and usable (there are online docs and a nice www gui),
-it's mostly interesting for hackers and developers from now, I didn't confront to real users yet :P
-
-See features list, it's mostly handy for people with large databases,
-with optionally multiple computers.
-It can be adapted to handle video too, hacking some bit of code.
+        With this package you can play your music in zicbee if mplayer is installed on your system
         ''',
-        url = 'http://box.gnux.info/zicbee/',
         keywords = 'database music tags metadata management',
         packages = find_packages(),
-        zip_safe = False,
-        package_data = {
-            'zicbee': [
-                'ui/web/templates/*.html',
-                'ui/web/static/*.css',
-                'ui/web/static/*.js',
-                'ui/web/static/pics/*.*',
-                'ui/web/static/pics/cmd/*.*',
-                'ui/gtk/*.glade'],
-            },
 
-        entry_points = {
-            "console_scripts": [
-                'zicdb = zicbee.core:startup',
-                'zicbee = zicbee.core:shell',
-                'zicserve = zicbee.core:serve [server]',
-#                'zicgui = zicbee.ui.gtk.player:main [player]'
-                ],
-            "setuptools.installation" : [
-                'eggsecutable = zicbee.core:startup'
-                ]
-            },
-
-        install_requires = requirements,
-
-        extras_require = dict(
-#            player='pyglet>=1.2',
-            server='web.py>=0.31',
-            ),
+        entry_points = """
+        [zicbee.player]
+        mplayer = zicbee_mplayer:Player
+        """,
 
         dependency_links = [
             'eggs',
-            'http://box.gnux.info/zicbee/files/',
+            'http://zicbee.gnux.info/files/',
             'http://webpy.org/',
             'http://buzhug.sourceforge.net/',
             'http://code.google.com/p/quodlibet/downloads/list',
                 'Intended Audience :: Developers',
 #                'Intended Audience :: End Users/Desktop',
                 'Operating System :: OS Independent',
+                'Operating System :: Microsoft :: Windows',
+                'Operating System :: POSIX',
                 'Programming Language :: Python',
                 'Environment :: Console',
                 'Environment :: No Input/Output (Daemon)',
                 'Environment :: X11 Applications',
+                'Natural Language :: English',
                 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
                 'Topic :: Internet :: WWW/HTTP :: Indexing/Search',
                 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+                'Topic :: Software Development',
+                'Topic :: Software Development :: Libraries :: Python Modules',
                 'Topic :: Multimedia :: Sound/Audio :: Players',
                 'Topic :: Multimedia :: Sound/Audio :: Players :: MP3',
-                'Topic :: Text Processing :: Markup'
+                'Topic :: Text Processing :: Markup',
                 'Topic :: Utilities',
                 ],
 

File tests/runtests.sh

-#!/bin/sh
-PYTHONPATH=../eggs/buzhug-1.4-py2.5.egg:../eggs/mutagen-1.14-py2.5.egg:../eggs/simplejson-2.0.4-py2.5.egg:../eggs/web.py-0.31-py2.5.egg nosetests

File tests/test_db.py

-import os
-import sys
-
-class TestBuzhug(object):
-    def setUp(self):
-        sys.path.append( os.path.pardir )
-        import run
-        run.scan_eggs()
-
-        import zicbee.db
-        self._renew_db()
-        assert len(self.db.db) == 0
-
-    def _renew_db(self):
-        TMP_DB_NAME = 'tmpdatabase'
-
-        try:
-            self.db.db.close()
-        except AttributeError:
-            pass
-
-        import zicbee.db
-        db = zicbee.db.Database(TMP_DB_NAME)
-        db.destroy()
-        self.db = zicbee.db.Database(TMP_DB_NAME)
-
-    def _populate(self):
-        import random
-        size = 3
-        db = self.db.db
-        for n_art in xrange(size):
-            for n_alb in xrange(size):
-                for n_title in xrange(size):
-                    db.insert(
-                            '/foo%d'%random.randint(0, 99999999999999999),
-                            u'',
-                            unicode('artist %d'%n_art),
-                            unicode('album %d'%n_alb),
-                            unicode('title %d'%n_title),
-                            random.randint(0, 10),
-                            random.randint(1, 1000),
-                            None, None )
-
-        assert len(db) == size**3
-
-    def test_tags_corruption(self):
-        self._populate()
-        idx = len(self.db) - 2
-        tags = u',jazz,rock,pop,'
-        self.db[idx].update(tags=tags)
-        elt = self.db[idx]
-        print elt
-        assert elt.tags == tags
-        assert elt.score is None
-

File zicbee/__init__.py

-from pkgutil import extend_path
-__path__ = extend_path(__path__, __name__)
-import zicbee.core.debug # init logging

File zicbee/core/__init__.py

-# vim: et ts=4 sw=4
-
-def serve():
-    startup('serve')
-
-def setup_db(db_name, args):
-    import zicbee.core.zshell as zshell
-    zshell.init(args, db_name)
-
-def parse_cmd(action='help', *arguments):
-    db_name = None
-    arguments = list(arguments)
-
-    if action == 'use':
-        db_name = arguments[0]
-        action = arguments[1]
-        del arguments[:2] # Remove "<db name> <action>"
-
-    if '::' in action:
-        params = action.split('::')
-        action = params.pop(0)
-        kparams = dict()
-        for p in list(params):
-            if '=' in p:
-                k, v = p.split('=', 1)
-                kparams[k] = v
-                params.remove(p)
-    else:
-        kparams = dict()
-        params = tuple()
-
-    return db_name, arguments, action, params, kparams
-
-def execute_cmd(action, *params, **kparams):
-    try:
-        import zicbee.core.commands as cmds
-        commands_dict = dict((i[3:], getattr(cmds, i)) for i in dir(cmds) if i.startswith('do_'))
-        commands_dict.get(action, cmds.do_help)(*params, **kparams)
-    except KeyboardInterrupt:
-        print "Abort!"
-
-def shell():
-    startup('shell')
-
-def startup(*args):
-    import os
-    import sys
-    db_name = None
-    if not args:
-        arguments = sys.argv[2:]
-        if len(sys.argv) > 1:
-            action = sys.argv[1]
-        else:
-            action = 'help'
-        # feature: you can use aliases like zicdb-bigdrive to automatically use "bigdrive" db
-        if '-' in sys.argv[0]:
-            db_name = sys.argv[0].split('-', 1)[1]
-    else:
-        action = args[0]
-        arguments = args[1:]
-
-    cmd_db_name, arguments, action, params, kparams = parse_cmd(action, *arguments)
-    setup_db(db_name or cmd_db_name, arguments)
-    execute_cmd(action, *params, **kparams)
-

File zicbee/core/commands/__init__.py

-# vim: et ts=4 sw=4
-
-from zicbee.db import Database, DB_DIR
-from zicbee.core import zshell
-from zicbee.core.zshell import DEFAULT_NAME
-from zicbee.core.zutils import get_help_from_func
-from zicbee.core.debug import DEBUG, log
-from zicbee.core.config import config, DB_DIR, defaults_dict
-from zicbee.core import parse_cmd, execute_cmd, setup_db # needed by shell command
-import urllib
-import itertools
-from time import time as get_time
-import socket
-
-try:
-    socket.setdefaulttimeout(int(config.socket_timeout)) # setsocket  timeout, for interactive cmds
-except Exception, e:
-    log.warning("unable to set socket timeout to '%s': %s.", config.socket_timeout, e)
-
-from .search import do_search
-from .scan import do_scan
-from .help import do_help
-from .get import do_get
-from .player import (do_play, do_pause,
-        do_next, do_prev, do_shuffle,
-        do_infos, do_playlist,
-        do_tag, do_rate)
-
-from cmd import Cmd
-class Shell(Cmd):
-    def __init__(self, prompt='ZicBee'):
-        Cmd.__init__(self)
-        self.history = dict(filename=None, value=[])
-        if config.enable_history:
-            try:
-                import os
-                history_file = os.path.join(DB_DIR, 'shell_history.txt')
-                self.history['filename'] = history_file
-                self.history['value'] = [l.rstrip() for l in file(history_file).readlines()]
-                import readline
-                for hist in self.history['value']:
-                    readline.add_history(hist)
-            except Exception, e:
-                print "No history loaded: %s"%e
-                self.history['value'] = []
-        else:
-            self.history['value'] = []
-
-        self.commands = [name for name, obj in globals().iteritems() if name.startswith('do_') and callable(obj)]
-        self._prompt = prompt
-        self._refresh_prompt()
-
-    def _refresh_prompt(self):
-        self.prompt = "[%s > %s]\n%s> "%(config.db_host, config.player_host, self._prompt)
-
-    def get_names(self):
-        return self.commands
-
-    def onecmd(self, line):
-        if not line:
-            line = 'help'
-
-        try:
-            cmd, arg = line.split(None, 1)
-        except:
-            cmd = line
-            arg = []
-        else:
-            arg = arg.split()
-        self.lastcmd = line
-        if cmd == '':
-            return self.default(line)
-        elif cmd in ('EOF', 'bye', 'exit', 'logout'):
-            # save history & exit
-            try:
-                hist_fd = file(self.history['filename'], 'w')
-                try:
-                    hist_size = int(config.history_size)
-                except ValueError:
-                    hist_size = 100 # default value
-                hist_fd.writelines("%s\n"%l for l in self.history['value'][-hist_size:])
-            except Exception, e:
-                print "Cannot save history file: %s."%(e)
-            raise SystemExit('bye bye')
-        else:
-            db_name, new_args, action, p, kw = parse_cmd(line.split(None, 1)[0], *arg)
-            if db_name:
-                # re-init db & args
-                setup_db(db_name, new_args)
-            else:
-                zshell.args[:] = new_args # remplace args with new args
-
-            try:
-                t0 = get_time()
-                execute_cmd(action, *p, **kw)
-                elapsed = get_time() - t0
-                if elapsed > 0.4:
-                    print "took %.1fs"%elapsed
-
-            except Exception, e:
-                print "ERROR: %s"%e
-            except KeyboardInterrupt:
-                print "Interrupted!"
-            else:
-                if not self.history['value'] or self.history['value'][-1] != line: # avoid consecutive clones
-                    self.history['value'].append(line)
-
-            self._refresh_prompt()
-
-def do_shell():
-    """ Starts a shell allowing you any command. """
-    shell = Shell()
-    shell._prompt = 'ZicBee'
-    shell.cmdloop('Welcome to zicbee, press ENTER for help.')
-
-def do_set():
-    """ set a config variable to the given value
-    list all variables and values if no argument is given"""
-
-    if not zshell.args:
-        # dumps *
-        values = defaults_dict.keys()
-#        print "[DEFAULT]"
-
-        not_found = []
-        for param in values:
-            try:
-                print "%s = %s"%(param, getattr(config, param))
-            except:
-                not_found.append(param)
-
-        if not_found:
-            print "unavaible: %s"%(', '.join(not_found))
-
-    elif zshell.args and len(zshell.args) < 4:
-        # (re)set a value
-
-        if len(zshell.args) == 2 or (len(zshell.args) == 3 and zshell.args[1] == '='):
-            # set a value
-            if zshell.args[1] == '=':
-                del zshell.args[1]
-            setattr(config, zshell.args[0], zshell.args[1])
-        elif len(zshell.args) == 1:
-            # reset (blank) a value
-            setattr(config, zshell.args[0], '')
-
-    else:
-        print """Takes exactly 2 arguments: set <variable> <value>, takes no param to list variables.
-        giving no arguments will print every variable
-        a single parameter will be reset (blanked) """
-
-def do_kill(host=None):
-    """ Kills the current db_host or any specified as argument """
-    if host is None:
-        host = config.db_host
-    play_uri = 'http://%s/db/kill'%(host)
-    try:
-        urllib.urlopen(play_uri).read()
-    except IOError:
-        print "RIP."
-
-def do_stfu(host=None):
-    """ Kills the current player_host
-    (in case db_host and player_host are the same, this command
-    is equivalent to "kill")
-    """
-    if host is None:
-        config.player_host
-    play_uri = 'http://%s/close'%(host)
-    try:
-        urllib.urlopen(play_uri).read()
-    except IOError:
-        print "Silence."
-
-def do_serve(pure=False):
-    """ Create a ZicDB instance
-    parameters:
-        pure (default: False): just start DB serving, no player
-    """
-    # chdir to serve files at the right place
-    import os, sys
-    from pkg_resources import resource_filename
-
-    p = os.path.dirname(resource_filename('zicbee.ui.web', 'static'))
-    os.chdir( p )
-
-    import web
-    from zicbee.core.httpdb import web_db_index
-
-    pid = 0 # if not forking, still execute children commands
-    do_detach = False # do not try to detach by default
-
-    if config.fork:
-        try:
-            pid = os.fork()
-            do_detach = True # fork succeded, try to detach
-        except Exception, e:
-            print "Can't fork: %s."%e
-
-    if pid == 0:
-
-        if do_detach:
-            os.setsid()
-
-        if not pure:
-            # let's do webplayer
-            try:
-                from zicbee.player.webplayer import webplayer
-            except RuntimeError:
-                web.debug("Can't load webplayer, falling-back to pure db mode")
-                DEBUG()
-                pure = True
-
-        sys.argv = ['zicdb', '0.0.0.0:%s'%(config.default_port)]
-        try:
-            print "Running web%s from %s"%('db' if pure else 'player', __file__)
-            if pure:
-                urls = ('/db/(.*)', 'web_db_index',
-                        '/(.*)', 'web_db_index')
-            else:
-                urls = ('/db/(.*)', 'web_db_index',
-                        '/(.*)', 'webplayer')
-            app = web.application(urls, locals())
-            app.run()
-        except:
-            DEBUG()
-            print os.kill(os.getpid(), 9)
-            #print 'kill', os.getpid()
-
-def do_list():
-    """ List available databases (some can be specified with "use" argument) """
-    from os import listdir
-    from os.path import isfile, join
-
-    for i in listdir(DB_DIR):
-        if isfile(join(DB_DIR, i, '__info__')) and isfile(join(DB_DIR, i, 'album')):
-            txt = "%s # %d records"%(i, len(Database(i)))
-            if i == DEFAULT_NAME:
-                txt += ' [default]'
-            print txt
-
-def do_debug():
-    """ Start a PDB (dev/hackers only)"""
-    import pdb; pdb.set_trace()
-
-def do_bundle():
-    """ Dump used database to specified archive (any filename) """
-    if len(zshell.args) != 1:
-        raise SystemExit("Need filename name as agment !")
-    zshell.songs.dump_archive(zshell.args[0])
-
-def do_reset():
-    """ Destroy all songs on used database """
-    zshell.songs.destroy()
-    print "Database cleared!"
-
-
-def do_hash():
-    """ List all songs by id and hash (mostly to debug find_dups command) """
-    for i in zshell.songs.get_hash_iterator():
-        print "%8d / %s"%i
-
-def do_find_dups(wpt=None, ar=None):
-    """
-    Find duplicates (WIP)
-    Parameters:
-        wpt: wrong positive threshold (ceil to not reach), default == auto
-        ar: auto remove (ask for directory deletion), the smallest directory always wins
-    """
-    import heapq
-    from os.path import dirname
-
-    hash_dict = dict()
-
-    cnt = itertools.count()
-    total_cnt = itertools.count()
-
-    if wpt is None:
-        wpt = min(1000, len(zshell.songs)/60) # take untaged/corrupted data into account
-
-    for num, footprint in zshell.songs.get_hash_iterator():
-        if footprint not in hash_dict:
-            hash_dict[footprint] = [num]
-        else:
-            hash_dict[footprint].append(num)
-
-    if ar:
-        for m in (matches for num, matches in hash_dict.iteritems() if 1 < len(matches) < wpt):
-            h = []
-            for num in m:
-                song = zshell.songs[num]
-                heapq.heappush(h,
-                        (len(song.filename), song))
-
-            for nb, other in h:
-                if other != h[-1][1]:
-                    print "rm '%s'"%(other.filename.replace("'", r"\'"))
-                else:
-                    print "# kept %s"%other.filename
-    else:
-        for m in (matches for num, matches in hash_dict.iteritems() if 1 < len(matches) < wpt):
-            print "#", cnt.next()
-            for num in m:
-                total_cnt.next()
-                print "%d: %s"%(num, zshell.songs[num].filename)
-        print total_cnt.next()-cnt.next()-1, "# songs to be removed..."
-
-
-def do_fullhelp():
-    """ The Hacker's help [read standard help before !!] (WIP functions included) """
-    g = globals()
-    undoc = []
-    command_functions = [g[name] for name in g.keys() if name[:3] == 'do_']
-    command_functions.sort()
-    commands_display = []
-    remote_commands_display = []
-    for cmd in command_functions:
-        cmd_help, cmd_is_remote = get_help_from_func(cmd)
-
-        if cmd_is_remote:
-            remote_commands_display.append(cmd_help)
-        else:
-            commands_display.append(cmd_help)
-
-        if not '\n' in cmd_help:
-            undoc.append(cmd.func_name[3:])
-
-    for cmd in itertools.chain( ['[REMOTE COMMANDS]\n'], remote_commands_display, ['[LOCAL COMMANDS]\n'], commands_display ):
-        print cmd
-    print "Not documented:", ', '.join(undoc)
-

File zicbee/core/commands/get.py

-__all__ = ['do_get']
-
-import os
-import sys
-import time
-import urllib
-from itertools import chain
-from weakref import WeakKeyDictionary
-
-from .search import do_search
-from zicbee.core.zutils import duration_tidy, safe_path
-from zicbee.core.config import config
-
-def DownloadGenerator(uri):
-    uri, filename = uri
-
-    if os.path.exists(filename):
-        return
-
-    site = urllib.urlopen(uri)
-    out_file = file(filename, 'w')
-    BUF_SZ = 2**16
-    try:
-        total_size = int(site.info().getheader('Content-Length'))
-    except TypeError:
-        total_size = None
-    actual_size = 0
-    progress_p = 0
-
-    while True:
-        data = site.read(BUF_SZ)
-        if not data:
-            out_file.close()
-            break
-        out_file.write(data)
-        actual_size += len(data)
-
-        if total_size:
-            percent = total_size/actual_size
-        else:
-            percent = actual_size
-
-        if percent != progress_p:
-            yield percent
-
-            progress_p = percent
-        else:
-            yield '.'
-
-
-class Downloader(object):
-    def __init__(self, nb_dl=2):
-        self._nb_dl = nb_dl
-        self._last_display = time.time()
-
-    def run(self, uri_list):
-        downloaders = [] # Generators to handle
-        in_queue = [] # List of "TODO" uris
-
-        _download_infos = dict(count=0, start_ts = time.time())
-        percent_memory = WeakKeyDictionary()
-        write_out = sys.stdout.write
-
-        def _download():
-            for dl in downloaders:
-                try:
-                    ret = dl.next()
-                except StopIteration:
-                    downloaders.remove(dl)
-                    _download_infos['count'] += 1
-                else:
-                    if isinstance(ret, int):
-                        percent_memory[dl] = ret
-
-            # Display things
-            t = time.time()
-
-            if self._last_display + 0.1 < t:
-                self._last_display = t
-                sumup = ', '.join('%3d%%'%(val if int(val)<=100 else 0)
-                        for val in percent_memory.itervalues())
-                write_out(' [ %s ] %d               \r'%(sumup, _download_infos['count']))
-
-                sys.stdout.flush()
-
-        for uri in chain( uri_list, in_queue ):
-            if len(downloaders) < self._nb_dl:
-                try:
-                    dg = DownloadGenerator(uri)
-                    dg.next() # start the pump
-                    downloaders.append( dg )
-                except StopIteration:
-                    pass
-            else:
-                in_queue.append(uri) # Later please !
-                # iterate
-                _download()
-
-        # Terminate the job
-        while downloaders:
-            _download()
-
-        t = time.time() - _download_infos['start_ts']
-        write_out("                         \nGot %d files in %s. Enjoy ;)\n"%(
-                _download_infos['count'], duration_tidy(t)))
-
-
-def do_get(host=None, out=None):
-    """ Get songs (same syntax as search)
-    See "help" for a more complete documentation
-    """
-    if out is None:
-        out = config.download_dir
-
-    if host is None:
-        host = config.db_host
-
-    if ':' not in host:
-        host = "%s:%s"%(host, config.default_port)
-
-    uri_list = []
-    def _append_uri_filename(args):
-        uri = args[0]
-        filename = os.path.join(out,
-                safe_path( ' - '.join(a for a in args[1:4] if a)
-                + args[0].split('?', 1)[0][-4:])
-                )
-        uri_list.append((uri, filename))
-
-    do_search(out=_append_uri_filename, host=host)
-    Downloader().run(uri_list)
-

File zicbee/core/commands/help.py

-# vim: et ts=4 sw=4
-from zicbee.db import valid_tags
-from zicbee.core import zshell
-from zicbee.core.zutils import get_help_from_func
-
-def do_help():
-        """ Show a nifty help about most used commands """
-        if zshell.args:
-            from zicbee.core import commands
-            not_found = []
-            for arg in zshell.args:
-                try:
-                    cmd = getattr(commands, 'do_%s'%arg)
-                except AttributeError:
-                    cmd = None
-                    not_found.append(arg)
-
-                if cmd:
-                    cmd_help, cmd_is_remote = get_help_from_func(cmd)
-                    print cmd_help
-            if not_found:
-                print "Not found: %s"%(', '.join(not_found))
-            return
-
-        print "Welcome to ZicDB!".center(80)
-        print """
-use
-    Not a command by itself, used to specify active database (default: songs)
-    You can specify mutiple databases at once using ',' separator (without spaces)
-    Exemple:
-    %% %(prog)s use lisa serve
-      > starts serving lisa's database
-    %% %(prog)s use usb_drive reset
-      > destroy usb_drive database
-    %% %(prog)s use ipod bundle ipod_database_backup.zdb
-      > backups the "ipod" database into "ipod_database_backup.zdb" file in current directory
-
-    WARNING: using more than one database will most probably lead to song conflicts and won't be usable
-      consider this feature very experimental, use only if you know what you are doing.
-
-    NOTE: you can alternatively use the "ZDB" environment variable instead
-
-serve[::pure]
-    Runs a user-accessible www server on port 8080
-
-  pure:
-    don't allow player functionality access
-
-list
-    List available Databases.
-
-reset
-    Erases the Database (every previous scan is lost!)
-
-bundle <filename>
-    Create a bundle (compressed archive) of the database
-
-scan <directory|archive> [directory|archive...]
-    Scan directories/archive for files and add them to the database
-
-get[::host][::out] <match command>
-  host:
-    the host to connect to
-
-  out:
-    the output directory
-
-  Example:
-    %% %(prog)s get::out=/tmp/download::host=gunter.biz artist: black
-    %% %(prog)s get::gunter.biz artist: black
-
-play[::dbhost::phost] <match command>
-    Set playlist to specified request and start playing if previously stopped
-
-    dbhost:
-      the computer owning the songs
-    phost:
-      the playback computer
-
-search[::out::host] <match command>
-
-  out:
-    specifies the output format (for now: m3u or null or default)
-
---- Match commands composition: ---
-
-    field: value [ [or|and] field2: value2 ]...
-    for length, value may be preceded by "<" or ">"
-    if field name starts with a capital, the search is case-sensitive
-
-  Possible fields:
-\t- id (compact style)
-\t- %(tags)s
-
-  Working Exemples:
-  %% %(prog)s search filename: shak length: > 3*60
-    > songs with "shak" in filename and of more than 3 min
-  %% %(prog)s search artist: shak length: > 3*60 or artist: black
-    > songs with "shak" or "black" in artist name and of more than 3 min
-  %% %(prog)s search artist: shak length: > 3*60 and title: mo
-    > songs with "shak" in artist name and of more than 3 min and with "mo" on the title
-  %% %(prog)s search tags: jazz title: moon
-    > songs tagged "jazz" and with titles containing "moon"
-  %% %(prog)s search score: >2
-    > songs with a score higher than 2
-  %% %(prog)s search score: >2 score: <= 3 tags: rock length: <= 120
-    > a quite dumb search ;) (the selected songs will have a score of 3, less than 2 min and tagged as "rock")
-
-fullhelp
-    List all available commands (with associated help if available)
-    """%dict(
-            tags = '\n\t- '.join(valid_tags),
-            prog = "zicdb")
-

File zicbee/core/commands/player.py

-__all__ = [
-    'do_play', 'do_pause',
-    'do_next', 'do_prev', 'do_shuffle',
-    'do_infos', 'do_playlist',
-    'do_tag', 'do_rate',
-]
-
-import urllib
-from zicbee.core.zshell import args
-from zicbee.core.config import config
-from .search import do_search
-
-def do_play(dbhost=None, phost=None):
-    """ Play the specified pattern, same syntax as "search". """
-
-    if phost is None:
-        phost = config.player_host
-
-    if dbhost is None:
-        dbhost = config.db_host
-    play_uri = 'http://%s/search?id=&host=%s&pattern=%s'%(phost, dbhost, urllib.quote(u' '.join(args)))
-    urllib.urlopen(play_uri).read()
-
-def do_infos(host=None):
-    """ Show informations about currently playing song """
-    if host is None:
-        host = config.player_host
-    play_uri = 'http://%s/infos?fmt=txt'%(host)
-    site = urllib.urlopen(play_uri)
-    while True:
-        l = site.readline()
-        if not l:
-            break
-        print l,
-
-def do_playlist(host=None):
-    """ Show current playing list """
-    if host is None:
-        host = config.player_host
-    play_uri = 'http://%s/playlist?fmt=txt'%(host)
-    site = urllib.urlopen(play_uri)
-    while True:
-        l = site.readline()
-        if not l:
-            break
-        print l,
-
-def do_pause(host=None):
-    """ Toggle pause on player """
-    if host is None:
-        host = config.player_host
-    play_uri = 'http://%s/pause'%(host)
-    urllib.urlopen(play_uri).read()
-
-def do_shuffle(host=None):
-    """ Shuffles the playing list (results in a random playlist) """
-    if host is None:
-        host = config.player_host
-    play_uri = 'http://%s/shuffle'%(host)
-    urllib.urlopen(play_uri).read()
-
-def do_next(host=None):
-    """ Switch to next track """
-    if host is None:
-        host = config.player_host
-    play_uri = 'http://%s/next'%(host)
-    urllib.urlopen(play_uri).read()
-
-def do_prev(host=None):
-    """ Switch to previous track """
-    if host is None:
-        host = config.player_host
-    play_uri = 'http://%s/prev'%(host)
-    urllib.urlopen(play_uri).read()
-
-def do_tag(tag, host=None):
-    """ Tag selected pattern with specified rating.
-    options:
-        tag
-        host=<db_host>
-        + same pattern as "search"
-    ex:
-        tag::jazz,funny artist: richard cheese
-        tag::rock artist: noir d
-    NOTE:
-     - without a search pattern it will tag the currently playing song
-     - multi-tagging is allowed by using "," separator (NO BLANK!)
-
-    """
-
-    def song_tagger(song):
-        uri = song[0]
-        sid = (song[0].rsplit('=', 1)[1])
-        tag_uri = uri[:uri.index('/db/')+3] + '/tag/%s/%s'%(sid, tag)
-        urllib.urlopen(tag_uri)
-
-    if host is None:
-        host = config.player_host
-
-    if args:
-        do_search(out=song_tagger, host=host, edit_mode=True)
-    else:
-        tag_uri = 'http://%s/tag/%s'%(host, tag)
-        urllib.urlopen(tag_uri)
-
-def do_rate(rate=1, host=None):
-    """ Rate selected pattern with specified rating.
-    options:
-        rate=1
-        host=<db_host if pattern given, else defaults to player host to tag current song>
-        + same pattern as "search" (optionnal)
-    ex:
-        rate::3:guntah.myhost.com artist: Brassens
-        rate::0 title: Very bad song artist: very bad artist
-     NOTE: without a search pattern it will rate the currently playing song
-        """
-
-    def song_rater(song):
-        uri = song[0]
-        sid = (song[0].rsplit('=', 1)[1])
-        rate_uri = uri[:uri.index('/db/')+3] + '/rate/%s/%s'%(sid, rate)
-        urllib.urlopen(rate_uri)
-
-    if args:
-        do_search(out=song_rater, host=host or config.db_host, edit_mode=True)
-    else:
-        rate_uri = 'http://%s/rate/%s'%(host or config.player_host, rate)
-        urllib.urlopen(rate_uri)
-
-

File zicbee/core/commands/scan.py

-# vim: et ts=4 sw=4
-import itertools
-import os
-import sys
-from time import time
-from zicbee.core import zshell
-from zicbee.core.debug import DEBUG
-from zicbee.core.zutils import duration_tidy, clean_path
-
-def do_scan():
-    """ Scan a directory for songs (fill Database)
-    See "help" for a more complete documentation
-    """
-    if not zshell.args:
-        sys.exit('At least one argument must be specified!')
-
-    newline_iterator = itertools.cycle(x == 20 for x in xrange(21))
-    orig_nb = len(zshell.songs)
-    start_t = time()
-
-    archives = []
-    directories = []
-
-    for path in zshell.args:
-        path = clean_path(path)
-        if os.path.isdir(path):
-            directories.append(path)
-        else:
-            archives.append(path)
-
-    def _scan(**kw):
-        print ', '.join(':'.join((k,v)) for k,v in kw.iteritems())
-        try:
-            for status_char in zshell.songs.merge(**kw):
-                print status_char,
-                if newline_iterator.next():
-                    print ''
-                sys.stdout.flush()
-        except Exception, e:
-            DEBUG()
-
-    for path in archives:
-        _scan(archive=path, db_name=zshell.DEFAULT_NAME)
-
-    for path in directories:
-        _scan(directory=path, db_name=zshell.DEFAULT_NAME)
-
-    elapsed = time() - start_t
-    delta = len(zshell.songs)-orig_nb
-    print "\nProcessed %d (%s%d) songs in %s (%.2f/s.)"%(
-            len(zshell.songs),
-            '-' if delta < 0 else '+',
-            delta,
-            duration_tidy(elapsed),
-            len(zshell.songs)/elapsed)
-
-

File zicbee/core/commands/search.py

-from time import time
-import sys
-from zicbee.db import valid_tags
-from zicbee.core import zshell
-from zicbee.core.zutils import duration_tidy, parse_line, jload
-from zicbee.core.config import config
-from zicbee.core.debug import log, DEBUG
-
-def do_search(out=None, host=None, edit_mode=False):
-    """ Search for song, display results.
-    See "help" for a more complete documentation
-    """
-    if host is None:
-        host = config.db_host
-
-    if ':' not in host:
-        host = "%s:%s"%(host, config.default_port)
-
-    duration = 0
-    start_t = time()
-
-    fields = list(valid_tags)
-    fields.remove('filename')
-    fields = tuple(fields)
-
-    if callable(out):
-        song_output = out
-    elif out == 'm3u':
-        def song_output(song):
-            print song[0]
-    elif out == 'null':
-        def song_output(song): pass
-    else:
-        def song_output(song):
-            txt = '%s :\n%s [%s, score: %s, tags: %s]'%(song[0],
-                    ' - '.join(str(i) for i in song[1:4]),
-                    duration_tidy(song[4]), song[5], song[6],
-                    )
-            print txt.decode('utf8').encode('utf8')
-
-    num = 0
-    if host is not None:
-        import urllib
-        params = {'pattern':' '.join(zshell.args)}
-        uri = 'http://%s/db/?json=1&%s'%(host, urllib.urlencode(params))
-        site = urllib.urlopen(uri)
-        while True:
-            line = site.readline()
-            if not line:
-                break
-            try:
-                r = jload(line)
-            except:
-                DEBUG()
-                for l in site.readlines():
-                    log.error(l.rstrip())
-                raise
-
-            song_output(r)
-            duration += r[4]
-            num += 1
-    else:
-        pat, kw = parse_line(' '.join(zshell.args))
-
-        if edit_mode:
-            search_fn = zshell.songs.u_search
-        else:
-            search_fn = zshell.songs.search
-
-        for num, res in enumerate(search_fn(None, pat, **kw)):
-            song_output(res)
-            duration += res.length
-
-    sys.stderr.write("# %d results in %s for a total of %s!\n"%(
-            num,
-            duration_tidy(time()-start_t),
-            duration_tidy(duration)
-            ))
-
-

File zicbee/core/config.py

-import os
-import atexit
-import ConfigParser
-
-__all__ = ['DB_DIR', 'defaults_dict', 'config']
-
-DB_DIR = os.path.expanduser(os.getenv('ZICDB_PATH') or '~/.zicdb')
-try: # Ensure personal dir exists
-    os.mkdir(DB_DIR)
-except:
-    pass
-
-defaults_dict = {
-        'streaming_file' : '/tmp/zsong',
-        'download_dir' : '/tmp',
-        'db_host' : 'localhost:9090',
-        'player_host' : 'localhost:9090',
-        'debug' : '',
-        'default_search' : '',
-        'history_size' : 50,
-        'default_port': '9090',
-        'web_skin' : '',
-        'fork': 'blank_me_to_stop_forking_on_serve_mode',
-        'socket_timeout': '30',
-        'enable_history': 'blank_to_disable',
-        }
-
-config_filename = os.path.join(DB_DIR, 'config.ini')
-
-class ConfigObj(object):
-
-    _cfg = ConfigParser.ConfigParser(defaults_dict)
-
-    def __init__(self):
-        if os.path.exists(config_filename):
-            self._cfg.read(config_filename)
-        else:
-            self._cfg.write(file(config_filename, 'w'))
-
-    def __setattr__(self, name, val):
-        if name.endswith('_host') and ':' not in val:
-            val = '%s:%s'%( val, self.default_port )
-        return self._cfg.set('DEFAULT', name, val)
-
-    def __getattr__(self, name):
-        return self._cfg.get('DEFAULT', name)
-
-# Ensure the file is written on drive
-atexit.register(lambda: config._cfg.write(file(config_filename, 'w')))
-
-config = ConfigObj()
-
-
-media_config = {
-        'mp3' : {'player_cache': 128, 'init_chunk_size': 2**18, 'chunk_size': 2**14},
-        'ogg' : {'player_cache': 128, 'init_chunk_size': 2**18, 'chunk_size': 2**14},
-        'mp4' : {'player_cache': 128, 'init_chunk_size': 2**18, 'chunk_size': 2**14},
-        'aac' : {'player_cache': 128, 'init_chunk_size': 2**18, 'chunk_size': 2**14},
-        'vqf' : {'player_cache': 128, 'init_chunk_size': 2**18, 'chunk_size': 2**14},
-        'wmv' : {'player_cache': 128, 'init_chunk_size': 2**18, 'chunk_size': 2**14},
-        'wma' : {'player_cache': 128, 'init_chunk_size': 2**18, 'chunk_size': 2**14},
-        'm4a' : {'player_cache': 128, 'init_chunk_size': 2**18, 'chunk_size': 2**14},
-        'asf' : {'player_cache': 128, 'init_chunk_size': 2**18, 'chunk_size': 2**14},
-        'oga' : {'player_cache': 128, 'init_chunk_size': 2**18, 'chunk_size': 2**14},
-        'flac' : {'player_cache': 4096, 'init_chunk_size': 2**22, 'chunk_size': 2**20},
-        }
-

File zicbee/core/debug.py

-__all__ = ['DEBUG', 'debug_enabled', 'log']
-import os
-import logging
-import traceback
-from logging import getLogger
-from zicbee.core.config import config
-
-log = getLogger('zicbee')
-
-default_formatter = logging.Formatter('[%(threadName)s %(relativeCreated)d] %(module)s %(funcName)s:%(lineno)s %(message)s')
-
-# add two handlers
-for h in logging.FileHandler('/tmp/zicbee.log'), logging.StreamHandler():
-    log.addHandler(h)
-    h.setFormatter( default_formatter )
-
-def traced(fn):
-    def _get_decorator(decorated):
-        def _decorator(*args, **kw):
-            try:
-                return decorated(*args, **kw)
-            except:
-                import pdb; pdb.set_trace()
-        return _decorator
-    return _get_decorator(fn)
-
-def DEBUG():
-    traceback.print_stack()
-    traceback.print_exc()
-
-debug_enabled = ('DEBUG' in os.environ) or config.debug
-
-if debug_enabled:
-    try:
-        val = logging.ERROR - int(os.environ.get('DEBUG', 1))*10
-    except ValueError:
-        val = logging.DEBUG
-
-    log.setLevel(val)
-else:
-    globals()['DEBUG'] = lambda: None # NO-OP if not debugging
-

File zicbee/core/httpdb.py

-# vim: et ts=4 sw=4
-from __future__ import with_statement
-
-__all__ = ['web_db_index', 'WEB_FIELDS', 'render']
-
-import os
-import web
-from threading import RLock
-from time import time
-from zicbee.core import zshell
-from zicbee.core.zutils import compact_int, jdump, parse_line, uncompact_int
-from zicbee.core.config import config
-
-WEB_FIELDS = 'artist album title length score tags'.split() + ['__id__']
-
-web.config.debug = True if config.debug and str(config.debug).lower() in ('on', 'yes') else False
-web.internalerror = web.debugerror
-
-# Set default headers & go to templates directory
-web.ctx.headers = [('Content-Type', 'text/html; charset=utf-8'), ('Expires', 'Thu, 01 Dec 1994 16:00:00 GMT')]
-from pkg_resources import resource_filename
-render = web.template.render(resource_filename('zicbee.ui.web', 'templates'))
-
-# DB part
-
-DbSimpleSearchForm = web.form.Form(
-        web.form.Hidden('id'),
-        web.form.Textbox('pattern'),
-        web.form.Checkbox('m3u'),
-        )
-
-def refresh_db():
-    zshell.songs.commit()
-    zshell.songs._create()
-    zshell.songs.cleanup()
-
-class web_db_index:
-
-    _db_lock = RLock()
-
-    def tag(self, song, tag):
-
-        song_id = uncompact_int(song)
-        try:
-            tag = unicode(tag)
-            with self._db_lock:
-                SEP=u','
-                _t = zshell.songs[song_id].tags
-                tag_set = set( _t.strip(SEP).split(SEP) ) if _t else set()
-                for single_tag in tag.split(','):
-                    tag_set.add(single_tag.strip())
-                new_tag = ''.join((SEP,SEP.join(tag_set),SEP))
-                zshell.songs[song_id].update(tags=new_tag)
-        except Exception, e:
-            web.debug('E!%s'%e)
-        finally:
-            refresh_db()
-
-    def rate(self, song, rating):
-        web.debug('rate: song=%s rating=%s'%(song,rating))
-        try:
-            with self._db_lock:
-                song_id = uncompact_int(song)
-                zshell.songs[song_id].update(score=int(rating))
-        finally:
-            refresh_db()
-
-    def multirate(self, ratings_list):
-        web.debug('rate: ratings_list=%s'%ratings_list)
-        try:
-            ratings = [rating.split('=') for rating in ratings_list.split(',')]
-            with self._db_lock:
-                for song, rating in ratings:
-                        song_id = uncompact_int(song)
-                        zshell.songs[song_id].update(score=int(rating))
-        finally:
-            refresh_db()
-
-    def GET(self, name):
-        hd = web.webapi.ctx.homedomain
-        t0 = time()
-        af = DbSimpleSearchForm()
-        if name.startswith('rate/'):
-            self.rate(*name.split('/', 3)[1:])
-            return
-        if name.startswith('multirate/'):
-            self.multirate(name.split('/', 2)[1])
-            return
-        elif name.startswith('kill'):
-            zshell.songs.close()
-            raise SystemExit()
-        elif name.startswith('tag'):
-            self.tag(*name.split('/', 3)[1:])
-            return
-        elif af.validates():
-            try:
-                af.fill()
-                song_id = af['id'].value
-                if song_id:
-                    song_id = uncompact_int(song_id)
-                    if name.startswith("get"):
-                        filename = zshell.songs[song_id].filename
-                        web.header('Content-Type', 'application/x-audio')
-                        web.header('Content-Disposition',
-                                'attachment; filename:%s'%filename.rsplit('/', 1)[-1], unique=True)
-
-                        CHUNK=1024
-                        in_fd = file(filename)
-                        web.header('Content-Length', str( os.fstat(in_fd.fileno()).st_size ) )
-                        yield
-
-                        while True:
-                            data = in_fd.read(CHUNK)
-                            if not data: break
-                            y = (yield data)
-                        return
-                    else:
-                        song = zshell.songs[song_id]
-                        for f in song.fields:
-                            yield "<b>%s</b>: %s<br/>"%(f, getattr(song, f))
-                        return
-            except GeneratorExit:
-                raise
-            except Exception, e:
-                web.debug(e)
-
-        if af['m3u'].value:
-            web.header('Content-Type', 'audio/x-mpegurl')
-            format = 'm3u'
-        elif web.input().get('plain'):
-            format = 'plain'
-        elif web.input().get('json'):
-            format = 'json'
-        else:
-            web.header('Content-Type', 'text/html; charset=utf-8')
-            format = 'html'
-
-        pattern = af['pattern'].value
-
-        if pattern is None:
-            res = None
-        else:
-            pat, vars = parse_line(pattern)
-            urlencode = web.http.urlencode
-            ci = compact_int
-            web.debug('searching %s %s...'%(pat, vars))
-
-            res = ([hd+'/db/get/%s?id=%s'%('song.'+ r.filename.rsplit('.', 1)[-1].lower(), ci(int(r.__id__))), r]
-                    for r in zshell.songs.search(list(WEB_FIELDS)+['filename'], pat, **vars)
-                    )
-        t_sel = time()
-
-        if format == 'm3u':
-            yield unicode(render.m3u(web.http.url, res))
-        elif format == 'plain':
-            yield unicode(render.plain(af, web.http.url, res))
-        elif format == 'json':
-            some_db = zshell.songs.databases.itervalues().next()['handle']
-            # try to pre-compute useful things
-            field_decoder = zip( WEB_FIELDS,
-                    (some_db.f_decode[some_db.fields[fname]] for fname in WEB_FIELDS)
-                    )
-            yield
-
-            infos_iterator = ( [r[0]] + [d(r[1][r[1].fields.index(f)]) for f, d in field_decoder]
-                    for r in res )
-            try:
-                # TODO: optimise this (jdump by 100 size blocks)
-                for r in infos_iterator:
-                    yield jdump(r)
-                    yield '\n'
-                web.debug('handled in %.2fs (%.2f for select)'%(time() - t0, t_sel - t0))
-            except Exception, e:
-                web.debug("ERR:", e)
-        else:
-            yield unicode(render.index(af, res))

File zicbee/core/zshell.py

-# vim: et ts=4 sw=4
-import os
-import sys
-from zicbee.db import Database
-
-DEFAULT_NAME='songs'
-args = []
-
-def init(args=None, db_name=None):
-    try:
-        db
-    except NameError:
-        pass
-    else:
-        print "db cleanup"
-        db.cleanup() # XXX: Ugly !
-        db.close()
-
-    db_name = db_name or os.environ.get('ZDB', DEFAULT_NAME)
-    print "opening %s..."%db_name
-    db = Database(db_name)
-    globals().update( dict(songs=db, args=args) )
-    db.cleanup() # XXX: Ugly !
-

File zicbee/core/zutils.py

-__all__ = ['jdump', 'jload', 'clean_path', 'safe_path', 'parse_line', 'duration_tidy', 'get_help_from_func', 'DEBUG']
-
-import traceback
-import itertools
-import inspect
-import string
-import sys
-import os
-from os.path import expanduser, expandvars, abspath
-from zicbee.core.debug import log # forward some symbols
-import logging
-
-# Filename path cleaner
-def clean_path(path):
-    return expanduser(abspath(expandvars(path)))
-
-def safe_path(path):
-    return path.replace(os.path.sep, ' ')
-
-# int (de)compacter [int <> small str convertors]
-# convert to base62...
-base = 62
-chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
-
-def compact_int(ival):
-    result = []
-    rest = ival
-    b = base
-    while True:
-        int_part, rest = divmod(rest, b)
-        result.append(chars[rest])
-        if not int_part:
-            break
-        rest = int_part
-    return "".join(reversed(result))
-
-def uncompact_int(str_val):
-    # int(x, base) not used because it's limited to base 36
-    unit = 1
-    result = 0
-    for char in reversed(str_val):
-        result += chars.index(char) * unit
-        unit *= base
-    return result
-
-################################################################################
-
-#
-# Try to get the most performant json backend
-#
-# cjson:
-# 10 loops, best of 3: 226 msec per loop
-# simplejson:
-# 1 loops, best of 3: 10.3 sec per loop
-# demjson:
-# 1 loops, best of 3: 65.2 sec per loop
-#
-
-json_engine = None
-try:
-    from json import dumps as jdump, loads as jload
-    json_engine = "python's built-in"
-except ImportError:
-    try:
-        from cjson import encode as jdump, decode as jload
-        json_engine = 'cjson'
-    except ImportError:
-        try:
-            from simplejson import dumps as jdump, loads as jload
-            json_engine = 'simplejson'
-        except ImportError:
-            from demjson import encode as jdump, decode as jload
-            json_engine = 'demjson'
-
-sys.stderr.write("using %s engine.\n"%json_engine)
-################################################################################
-
-_plur = lambda val: 's' if val > 1 else ''
-
-def duration_tidy(orig):
-    minutes, seconds = divmod(orig, 60)
-    if minutes > 60:
-        hours, minutes = divmod(minutes, 60)
-        if hours > 24:
-            days, hours = divmod(hours, 24)
-            return '%d day%s, %d hour%s %d min %02.1fs.'%(days, _plur(days), hours, _plur(hours), minutes, seconds)
-        else:
-            return '%d hour%s %d min %02.1fs.'%(hours, 's' if hours>1 else '', minutes, seconds)
-    else:
-        return '%d min %02.1fs.'%(minutes, seconds)
-    if minutes > 60:
-        hours = int(minutes/60)
-        minutes -= hours*60
-        if hours > 24:
-            days = int(hours/24)
-            hours -= days*24
-            return '%d days, %d:%d.%ds.'%(days, hours, minutes, seconds)
-        return '%d:%d.%ds.'%(hours, minutes, seconds)
-    return '%d.%02ds.'%(minutes, seconds)
-
-################################################################################
-# documents a function automatically
-
-def get_help_from_func(cmd):
-    """
-    returns a tuple (str::tidy doc, bool::is_remote) from a function
-    """
-    arg_names, not_used, neither, dflt_values = inspect.getargspec(cmd)
-    is_remote = any(h for h in arg_names if h.startswith('host') or h.endswith('host'))
-
-    if cmd.__doc__:
-        if dflt_values is None:
-            dflt_values = []
-        else:
-            dflt_values = list(dflt_values)
-
-        # Ensure they have the same length
-        if len(dflt_values) < len(arg_names):
-            dflt_values = [None] * (len(dflt_values) - len(arg_names))
-#            map(None, arg_names, dflt_values)
-
-        doc = '::'.join('%s%s'%(arg_name, '%s'%('='+str(arg_val) if arg_val is not None else '')) for arg_name, arg_val in itertools.imap(None, arg_names, dflt_values))
-
-        return ("%s\n%s\n"%(
-            ("%s[::%s]"%(cmd.func_name[3:], doc) if len(doc) > 1 else cmd.func_name[3:]), # title
-            '\n'.join('   %s'%l for l in cmd.__doc__.split('\n') if l.strip()), # body
-        ), is_remote)
-    else:
-        return (cmd.func_name[3:], is_remote)
-
-################################################################################
-# line parser
-
-properties = []
-for name in 'score tags length artist title album filename'.split():
-    properties.append(name)
-    properties.append(name.title())
-del name
-
-def _find_property(line):
-    tab = line.rsplit(None, 1)
-    for prop in properties:
-        if prop == tab[-1]:
-            if len(tab) == 1:
-                return line
-            else:
-                return tab[-1], tab[0].strip()
-
-def _conv_line(txt):
-    """ Converts a syntax string to an easy to parse array of datas
-    data consists of str or (str, str) tuples
-    str values are operators like: or, and, !or, (, ), etc...
-    tuples are: key: value
-    """
-    # TODO: replace with a real parser ?
-    split_line = txt.split(':')
-    log.debug('split line: %s'% split_line)
-
-    if len(split_line) > 1:
-        ret = []
-        attr = None
-        for elt in split_line:
-            if attr is None:
-                if elt[0] == '(':