[SCM] mpd-sima/master: Imported Upstream version 0.12.0~4ae3d6a

kaliko-guest at users.alioth.debian.org kaliko-guest at users.alioth.debian.org
Sun Sep 28 11:34:26 UTC 2014


The following commit has been merged in the master branch:
commit ea12dcce42294a8eae60ba2a69dfdeb9e16af8c8
Author: Geoffroy Youri Berret <efrim at azylum.org>
Date:   Tue Jun 10 16:46:15 2014 +0200

    Imported Upstream version 0.12.0~4ae3d6a

diff --git a/README b/README
index 373ef5d..00360ca 100644
--- a/README
+++ b/README
@@ -1,7 +1,9 @@
 Design for python >= 3.3
 
-Requires python-musicpd:
+Requires: python-musicpd >= 0.4.0 [0],
+          requests >= 2.2.0 [1]
 
-		http://media.kaliko.me/src/musicpd/
+	[0] http://media.kaliko.me/src/musicpd/
+	[1] http://docs.python-requests.org/
 
 
diff --git a/data/bash/completion.sh b/data/bash/completion.sh
new file mode 100644
index 0000000..be0d444
--- /dev/null
+++ b/data/bash/completion.sh
@@ -0,0 +1,121 @@
+# Copyright (c) 2010, 2011, 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of MPD_sima
+#
+#  MPD_sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  MPD_sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with MPD_sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+# Bash completion file
+#
+# On debian system either place this file in etc/bash_completion.d/ or source it
+# in your barshrc.
+
+_sima() {
+    local cur prev opts
+    COMPREPLY=()
+    _get_comp_words_by_ref cur prev
+    opts="-c --config \
+          -p --pid \
+          -l --log \
+          -v --log-level \
+          -S --host \
+          -P --mpd_port \
+          -h --help --version \
+          --var_dir \
+          -d --daemon"
+
+    if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
+        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+        return 0
+    fi
+
+    case "${prev}" in
+        --var_dir)
+            _filedir -d
+            ;;
+        -v|--log-level)
+            COMPREPLY=( $(compgen -W "debug info warning error" -- ${cur} ))
+            ;;
+        -p|--pid|-l|--log)
+            _filedir
+            ;;
+        -c|--config)
+            _filedir
+            if [ -z $XDG_DATA_HOME ]; then
+                local confnames=$(for x in $(ls -1 $HOME/.config/mpd_sima/*.cfg 2>/dev/null) ; do echo "${x##*//}"; done)
+            else
+                local confnames=$(for x in $(ls -1 $HOME/.config/mpd_sima/*.cfg $XDG_DATA_HOME/mpd_sima/*.cfg 2>/dev/null) ; do echo "${x##*//}"; done)
+            fi
+            COMPREPLY+=( $(compgen -W "${confnames}") )
+            return 0
+            ;;
+        --host|-S)
+            COMPREPLY=( $(compgen -A hostname ${cur}) )
+            ;;
+        *)
+            ;;
+    esac
+}
+complete -F _sima mpd_sima
+complete -F _sima mpd-sima
+
+_art_names_list() {
+    local IFS=$'\n'
+    compgen -W "${artists}" -- ${cur}
+}
+
+_simadb_cli() {
+    local cur prev opts artists
+    local IFS=$'\n'
+    COMPREPLY=()
+    _get_comp_words_by_ref cur prev
+    opts="--add_similarity -a --remove_similarity --remove_artist \
+    --purge_hist --view_artist --view_all \
+    --bl_curr_trk --bl_curr_art --bl_curr_al --bl_art --remove_bl --view_bl \
+    --dbfile -d \
+    --host -S --port -P \
+    --reciprocal -r --check_names -c \
+    --version -h --help"
+    opts=$(echo $opts | sed 's/ /\n/g')
+
+    if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
+        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+        return 0
+    fi
+
+    case "${prev}" in
+        --bl_curr*|--view_bl|--view_all|--purge_hist|--version|--help|-h)
+            return 0
+            ;;
+        -d|--dbfile)
+            _filedir
+            ;;
+        --host|-S)
+            COMPREPLY=( $(compgen -A hostname ${cur}) )
+            ;;
+        -a|--add_similarity|--view_artist|-v|--bl_art)
+            if [ -x /usr/bin/mpc ]; then
+                artists=$(for x in $(/usr/bin/mpc list artist) ; do echo "'${x}'"; done)
+                COMPREPLY=( $(compgen -W "${artists}" -- ${cur}) )
+                return 0
+            fi
+            # It should also complete artist name when the string ends with a comma
+            return 0
+            ;;
+        *)
+            ;;
+    esac
+}
+complete -F _simadb_cli simadb_cli
diff --git a/data/Makefile b/data/man/Makefile
similarity index 84%
rename from data/Makefile
rename to data/man/Makefile
index 0e6b1f3..67fb872 100644
--- a/data/Makefile
+++ b/data/man/Makefile
@@ -17,7 +17,7 @@ OPTIONS=--nonet\
 all: man
 
 simadb_cli: simadb_cli.1.xml
-	$(XP) $(OPTIONS) $(XSL) $<
+	xmllint --xinclude --nowarning --noent $< | $(XP) $(OPTIONS) $(XSL) -
 
 mpd_sima: mpd_sima.1.xml
 	xmllint --xinclude --nowarning --noent $< | $(XP) $(OPTIONS) $(XSL) -
@@ -49,4 +49,11 @@ valid:
 clean_mpd_sima:
 	rm -rf mpd-sima.1 mpd_sima.1.html
 
-clean: clean_mpd_sima
+clean_simadb_cli:
+	rm -rf simadb_cli.1 simadb_cli.1.html
+
+clean_mpd_sima.cfg:
+	rm -rf mpd_sima.cfg.5 mpd_sima.cfg.5.html
+
+clean: clean_mpd_sima clean_simadb_cli clean_mpd_sima.cfg
+	rm -rf ./*.pdf
diff --git a/data/man/album.cfg b/data/man/album.cfg
new file mode 100644
index 0000000..645be84
--- /dev/null
+++ b/data/man/album.cfg
@@ -0,0 +1,15 @@
+[MPD]
+host=example.org
+port=8000
+
+[sima]
+history_duration=48  # 2 days
+queue_length=5
+
+[lastfm]
+queue_mode = album
+album_to_add=2
+
+[crop]
+# keep 30 played tracks in playlist
+consume = 30
diff --git a/data/feedback.xml b/data/man/feedback.xml
similarity index 100%
rename from data/feedback.xml
rename to data/man/feedback.xml
diff --git a/data/man/files.xml b/data/man/files.xml
new file mode 100644
index 0000000..992d2b4
--- /dev/null
+++ b/data/man/files.xml
@@ -0,0 +1,32 @@
+<?xml version='1.0' encoding='UTF-8'?>
+    <refsect1 id="files">
+        <title>FILES</title>
+        <variablelist>
+          <varlistentry>
+              <term><filename>${XDG_CONFIG_HOME}/mpd_sima/mpd_sima.cfg</filename></term>
+              <listitem>
+                  <para>Configuration file.</para>
+              </listitem>
+          </varlistentry>
+            <varlistentry>
+                <term><filename>${XDG_DATA_HOME}/mpd_sima/sima.db</filename></term>
+                <listitem>
+                    <para>SQLite DB file.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry>
+                <term><filename>${XDG_DATA_HOME}/mpd_sima/WEB_SERVICE/</filename></term>
+                <listitem>
+                    <para>Persistant http cache.</para>
+                </listitem>
+            </varlistentry>
+        </variablelist>
+        <para>Usually <envar>XDG_DATA_HOME</envar> is set to
+            <filename>${HOME}/.local/share</filename> and <envar>XDG_CONFIG_HOME</envar> to
+            <filename>${HOME}/.config</filename>.<sbr />You may override them using
+            command line option <option>--var_dir</option> (cf.
+            <citerefentry><refentrytitle>mpd_sima</refentrytitle>
+                <manvolnum>1</manvolnum></citerefentry>)</para>
+    </refsect1>
+<!-- vim:filetype=docbk
+-->
diff --git a/data/info.xml b/data/man/info.xml
similarity index 100%
rename from data/info.xml
rename to data/man/info.xml
diff --git a/data/mpd-sima.1 b/data/man/mpd-sima.1
similarity index 94%
rename from data/mpd-sima.1
rename to data/man/mpd-sima.1
index faae9d9..9fedc50 100644
--- a/data/mpd-sima.1
+++ b/data/man/mpd-sima.1
@@ -2,12 +2,12 @@
 .\"     Title: mpd-sima
 .\"    Author: Jack Kaliko <kaliko at azylum.org>
 .\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
-.\"      Date: 01/25/2014
+.\"      Date: 06/10/2014
 .\"    Manual: mpd-sima 0.12.0 User Manual
 .\"    Source: mpd-sima
 .\"  Language: English
 .\"
-.TH "MPD\-SIMA" "1" "01/25/2014" "mpd-sima" "mpd-sima 0.12.0 User Manual"
+.TH "MPD\-SIMA" "1" "06/10/2014" "mpd-sima" "mpd-sima 0.12.0 User Manual"
 .\" -----------------------------------------------------------------
 .\" * Define some portability stuff
 .\" -----------------------------------------------------------------
@@ -196,19 +196,32 @@ the section called \(lqENVIRONMENT\(rq
 .PP
 ${XDG_CONFIG_HOME}/mpd_sima/mpd_sima\&.cfg
 .RS 4
-The per\-user configuration file\&. Usually
-\fBXDG_CONFIG_HOME\fR
-is set to
-${HOME}/\&.config\&.
+Configuration file\&.
 .RE
 .PP
 ${XDG_DATA_HOME}/mpd_sima/sima\&.db
 .RS 4
-SQLite database\&. Usually
+SQLite DB file\&.
+.RE
+.PP
+${XDG_DATA_HOME}/mpd_sima/WEB_SERVICE/
+.RS 4
+Persistant http cache\&.
+.RE
+.PP
+Usually
 \fBXDG_DATA_HOME\fR
 is set to
-${HOME}/\&.local/share\&.
-.RE
+${HOME}/\&.local/share
+and
+\fBXDG_CONFIG_HOME\fR
+to
+${HOME}/\&.config\&.
+.br
+You may override them using command line option
+\fB\-\-var_dir\fR
+(cf\&.
+\fBmpd_sima\fR(1))
 .SH "ENVIRONMENT"
 .PP
 \fBMPD_HOST\fR, \fBMPD_PORT\fR
@@ -233,7 +246,7 @@ mpd_sima\&.cfg
 .RS 4
 mpd_sima\&.cfg
 is read if present\&. Otherwise built\-in defaults are used\&. An example should be provided in the tarball within
-doc/examples/mpd_sima\&.cfg\&. On Debian system please look in
+doc/examples/\&. On Debian system please look in
 /usr/share/doc/mpd\-sima\&.
 .RE
 .PP
@@ -246,6 +259,9 @@ The default behavior is to add one track, this track is to be chosen among title
 To change these defaults, use the configuration file
 mpd_sima\&.cfg
 .RE
+.PP
+For details about mpd_sima\&.cfg refer to the manual
+\fBmpd_sima.cfg\fR(5)
 .SH "FEEDBACK/BUGS"
 .PP
 The maintainer would be more than happy to ear from you, don\*(Aqt hesitate to send feedback,
@@ -257,8 +273,7 @@ users are welcome to join the dedicated chat room at
 .SH "SEE ALSO"
 .PP
 \fBmpc\fR(1),
-\fBmpd\fR(1),
-\fBmpd-sima.cfg\fR(5)
+\fBmpd\fR(1)
 .PP
 /usr/share/doc/mpd\-sima/
 .SH "AUTHOR"
diff --git a/data/mpd_sima.1.xml b/data/man/mpd_sima.1.xml
similarity index 88%
rename from data/mpd_sima.1.xml
rename to data/man/mpd_sima.1.xml
index 44d77c4..5362cb9 100644
--- a/data/mpd_sima.1.xml
+++ b/data/man/mpd_sima.1.xml
@@ -204,33 +204,7 @@ man(1), man(7), http://www.tldp.org/HOWTO/Man-Page/
         </varlistentry>
     </variablelist>
   </refsect1>
-  <refsect1 id="files">
-      <title>FILES</title>
-      <variablelist>
-          <varlistentry>
-              <term><filename>${XDG_CONFIG_HOME}/mpd_sima/mpd_sima.cfg</filename></term>
-              <!--
-              <listitem>
-              </listitem>
-              <term><filename>${HOME}/.config/mpd_sima/mpd_sima.cfg</filename></term>
-              -->
-              <listitem>
-                  <para>The per-user configuration file. Usually <envar>XDG_CONFIG_HOME</envar> is set to <filename>${HOME}/.config</filename>.</para>
-              </listitem>
-          </varlistentry>
-          <varlistentry>
-              <term><filename>${XDG_DATA_HOME}/mpd_sima/sima.db</filename></term>
-              <!--
-              <listitem>
-              </listitem>
-              <term><filename>${HOME}/.local/share/mpd_sima/history.pkl</filename></term>
-              -->
-              <listitem>
-                  <para>SQLite database. Usually <envar>XDG_DATA_HOME</envar> is set to <filename>${HOME}/.local/share</filename>.</para>
-              </listitem>
-          </varlistentry>
-      </variablelist>
-  </refsect1>
+  <xi:include href="files.xml" />
   <refsect1 id="environment">
       <title>ENVIRONMENT</title>
       <variablelist>
@@ -257,7 +231,7 @@ man(1), man(7), http://www.tldp.org/HOWTO/Man-Page/
                   <para><filename>mpd_sima.cfg</filename> is read if present.
                       Otherwise built-in defaults are used. An example should be
                       provided in the tarball within
-                      <filename>doc/examples/mpd_sima.cfg</filename>. On Debian
+                      <filename>doc/examples/</filename>. On Debian
                       system please look in
                       <filename>/usr/share/doc/&dhpackage;</filename>.</para>
               </listitem>
@@ -275,25 +249,13 @@ man(1), man(7), http://www.tldp.org/HOWTO/Man-Page/
               </listitem>
           </varlistentry>
       </variablelist>
-  </refsect1>
-  <xi:include href="feedback.xml" />
-  <refsect1 id="see_also">
-    <title>SEE ALSO</title>
-    <!-- In alpabetical order. -->
-    <para>
-        <citerefentry>
-            <refentrytitle>mpc</refentrytitle>
-            <manvolnum>1</manvolnum>
-            </citerefentry>, <citerefentry>
-            <refentrytitle>mpd</refentrytitle>
-            <manvolnum>1</manvolnum>
-            </citerefentry>, <citerefentry>
-            <refentrytitle>mpd-sima.cfg</refentrytitle>
-            <manvolnum>5</manvolnum>
-        </citerefentry>
-    </para>
-      <para>
-          <filename>/usr/share/doc/mpd-sima/</filename>
+      <para>For details about mpd_sima.cfg refer to the manual
+          <citerefentry>
+              <refentrytitle>mpd_sima.cfg</refentrytitle>
+              <manvolnum>5</manvolnum>
+          </citerefentry>
       </para>
   </refsect1>
+  <xi:include href="feedback.xml" />
+  <xi:include href="seealso.xml" />
 </refentry>
diff --git a/data/man/mpd_sima.cfg.5 b/data/man/mpd_sima.cfg.5
new file mode 100644
index 0000000..95bc337
--- /dev/null
+++ b/data/man/mpd_sima.cfg.5
@@ -0,0 +1,413 @@
+'\" t
+.\"     Title: mpd_sima.cfg
+.\"    Author: Jack Kaliko <kaliko at azylum.org>
+.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
+.\"      Date: 06/10/2014
+.\"    Manual: mpd-sima 0.12.0 User Manual
+.\"    Source: mpd-sima
+.\"  Language: English
+.\"
+.TH "MPD_SIMA\&.CFG" "5" "06/10/2014" "mpd-sima" "mpd-sima 0.12.0 User Manual"
+.\" -----------------------------------------------------------------
+.\" * Define some portability stuff
+.\" -----------------------------------------------------------------
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.\" http://bugs.debian.org/507673
+.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.ie \n(.g .ds Aq \(aq
+.el       .ds Aq '
+.\" -----------------------------------------------------------------
+.\" * set default formatting
+.\" -----------------------------------------------------------------
+.\" disable hyphenation
+.nh
+.\" disable justification (adjust text to left margin only)
+.ad l
+.\" -----------------------------------------------------------------
+.\" * MAIN CONTENT STARTS HERE *
+.\" -----------------------------------------------------------------
+.SH "NAME"
+mpd_sima.cfg \- mpd\-sima will try to maintain some titles ahead in your play list following different policies\&. This manual document the configuration file for mpd\-sima\&.
+.SH "DESCRIPTION"
+.PP
+This manual page documents briefly
+\fBmpd\-sima\fR
+configuration options available in user configuration file (see
+the section called \(lqFILES\(rq)\&.
+.SH "EXAMPLES"
+.SS "Album queue mode\&."
+.PP
+Here is an example of album queue configuration\&.
+.sp
+.if n \{\
+.RS 4
+.\}
+.nf
+[MPD]
+host=example\&.org
+port=8000
+
+[sima]
+history_duration=48  # 2 days
+queue_length=5
+
+[lastfm]
+queue_mode = album
+album_to_add=2
+
+[crop]
+# keep 30 played tracks in playlist
+consume = 30
+
+            
+.fi
+.if n \{\
+.RE
+.\}
+.SH "CONFIGURATION FILE"
+.PP
+The configuration file consists of sections, led by a
+\fB[section]\fR
+header and followed by
+\fBname:\ \&value\fR
+entries, with continuations in the style of RFC 822 (see section 3\&.1\&.1, \(lqLONG HEADER FIELDS\(rq);
+\fBname=value\fR
+is also accepted\&. Lines beginning with
+\fI\*(Aq#\*(Aq\fR
+or
+\fI\*(Aq;\*(Aq\fR
+are ignored and may be used to provide comments (\fINota Bene:\fR
+inline comment are possible using
+\fI\*(Aq#\*(Aq\fR)\&.
+.PP
+The default values are used in the options lists below\&.
+.SS "MPD section"
+.PP
+This section is meant to configure MPD access, MPD host address / port and password if necessary\&.
+.PP
+\fB[MPD]\fR
+.RS 4
+.RE
+.PP
+\fBhost=\fR\fIlocalhost\fR
+.RS 4
+Set MPD host\&. Use IP or FQDN\&.
+.RE
+.PP
+\fBport=\fR\fI6600\fR
+.RS 4
+Set host port to access MPD to\&.
+.RE
+.PP
+\fBpassword=\fR\fIs3cr3t\fR
+.RS 4
+Set MPD password to use\&. Do not use this option if you don\*(Aqt have enabled password protected access on your MPD server\&.
+.RE
+.SS "log section"
+.PP
+Configure logging\&.
+.PP
+\fB[log]\fR
+.RS 4
+.RE
+.PP
+\fBlogfile=\fR
+.RS 4
+File to log to, usually in d\(aemon mode\&.
+.br
+Defaut (empty or unset) is to log to stdin/stdout\&.
+.RE
+.PP
+\fBverbosity=\fR\fIinfo\fR
+.RS 4
+Logging verbosity among
+\fIdebug\fR,
+\fIinfo\fR,
+\fIwarning\fR,
+\fIerror\fR\&.
+.RE
+.SS "sima section"
+.PP
+This section allows you to tweak core mpd_sima\&.cfg configuration\&.
+.PP
+\fB[sima]\fR
+.RS 4
+.RE
+.PP
+\fBhistory_duration=\fR\fI8\fR
+.RS 4
+How far to look back in history to avoid to play twice the same track/title (duration in hours)\&.
+.RE
+.PP
+\fBqueue_length=\fR\fI1\fR
+.RS 4
+This value triggers queue process if the play list length is less than specified queue_length\&.
+.RE
+.PP
+\fBuser_db=\fR\fIfalse\fR
+.RS 4
+Temporarily removed feature
+.RE
+.PP
+mpd\-sima\*(Aqs plugin management for internal source plugin and contrib (ie\&. external plugins)\&.
+.br
+
+Plugins list is a comma separated string list\&.
+.br
+
+Optional plugin\*(Aqs configuration lays in its own section\&.
+.br
+For instance a "AwesomePlugin" declared here gets its configuration from the corresponding section "[awesomeplugin]"\&.
+.br
+internal plugins will look for a section named after the lower\-cased name of the pluglin, ie\&. RandomFallBack\ \&\(-> randomfallback\&.
+.PP
+\fBinternal=\fR\fICrop, RandomFallBack, Lastfm\fR
+.RS 4
+\fBCrop\fR
+and
+\fBRandomFallback\fR
+are utilities plugins while
+\fBLastfm\fR
+is the actual queue plugin\&.
+.br
+
+Another queue plugin is available as a "techno preview", it relies on EchoNest web services, replace
+\fBLastFm\fR
+with
+\fBEchoNest\fR
+to try\&.
+.RE
+.PP
+\fBcontrib=\fR
+.RS 4
+.RE
+.SS "Crop section"
+.PP
+crop plugin\*(Aqs configuration:
+.PP
+\fB[crop]\fR
+.RS 4
+.RE
+.PP
+\fBconsume=\fR\fI0\fR
+.RS 4
+How many played tracks to keep in the play list\&. Allow to maintain a fixed length play list\&. Set to 0 to keep all played tracks\&.
+.RE
+.SS "RandomFallback section"
+.PP
+RandomFallback plugin\*(Aqs configuration:
+.PP
+\fB[randomfallback]\fR
+.RS 4
+.RE
+.PP
+\fBflavour=\fR\fIsensible\fR
+.RS 4
+When no similar tracks are found, falling back to random queuing\&. Different mode, aka random flavour, are available:
+\fIpure\fR,
+\fIsensible\fR,
+\fIgenre\fR\&.
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+\fIpure\fR, pure random choice, even among recently played track\&.
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+\fIsensible\fR, use play history to filter chosen tracks\&.
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+\fIgenre\fR, Not implemented yet\&.
+.RE
+.sp
+.RE
+.SS "LastFm section"
+.PP
+This section allows you to tweak LastFM plugin\*(Aqs configuration\&.
+.PP
+\fB[lastfm]\fR
+.RS 4
+.RE
+.PP
+\fBqueue_mode=\fR\fItrack\fR
+.RS 4
+Queue mode to use among
+\fItrack\fR,
+\fItop\fR
+and
+\fIalbum\fR
+(see
+the section called \(lqQUEUE MODES\(rq
+for info about queue modes)\&.
+.RE
+.PP
+\fBmax_art=\fR\fI10\fR
+.RS 4
+Number of similar artist to retrieve from local media library\&.
+.br
+When set to something superior to zero, it tries to get as much similar artists from media library\&.
+.RE
+.PP
+\fBdepth=\fR\fI1\fR
+.RS 4
+How many artists to base on similar artists search\&.
+.br
+
+The first is the last played artist and so on back in the history\&. Highter depth allows to get wider suggestions, it might help to reduce looping over same artists\&.
+.RE
+.PP
+\fBsingle_album=\fR\fIfalse\fR
+.RS 4
+Prevent from queueing a track from the same album (it often happens with OST)\&.
+.br
+
+Only relevant in "track" queue mode\&.
+.RE
+.PP
+\fBtrack_to_add=\fR\fI1\fR
+.RS 4
+How many track(s) to add\&. Only relevant in
+\fBtop\fR
+and
+\fBtrack\fR
+queue modes\&.
+.RE
+.PP
+\fBalbum_to_add=\fR\fI1\fR
+.RS 4
+How many album(s) to add\&. Only relevant in
+\fBalbum\fR
+queue modes\&.
+.RE
+.PP
+\fBcache=\fR\fITrue\fR
+.RS 4
+Whether or not to use on\-disk persistent http cache\&.
+.br
+When set to "true", sima will use a persistent cache for its http client\&. The cache is written along with the dbfile in:
+.br
+$XDG_CONFIG_HOME/mpd_sima/http/WEB_SERVICE\&.
+.br
+
+If set to "false", caching is still done but in memory\&.
+.RE
+.SH "QUEUE MODES"
+.PP
+mpd\-sima offers different queue modes\&. All of them pick up tracks from artists similar to the one currently played\&.
+.PP
+mpd\-sima tries preferably to chose among unplayed artists or at least not recently played artist\&. Concerning track and album queue modes titles are chosen purely at random among unplayed tracks\&.
+.PP
+\fBtrack\fR
+.RS 4
+Queue a similar track chosen at random from a similar artist\&.
+.RE
+.PP
+\fBtop\fR
+.RS 4
+Queue a track from a similar artist, chosen among "top tracks" according to last\&.fm data mining\&.
+.RE
+.PP
+\fBalbum\fR
+.RS 4
+Queue a whole album chosen at random from a similar artist\&.
+.sp
+\fINota Bene:\fR
+.br
+
+Due to the track point of view of database build upon tracks tags an album lookup for a specific artist will return albums as soon as this artist appears in a single track of the album\&.
+.br
+
+For instance looking for album from "The Velvet Underground" will fetch "Last Days" and "Juno" OSTs because the band appears on the soundtrack of these two movies\&.
+.br
+
+A solution is for you to set AlbumArtists tag to something different than the actual artist of the track\&. For compilations, OSTs etc\&. a strong convention is to use "Various Artists" for this tag\&.
+.sp
+mpd\-sima is currently looking for AlbumArtists tags and avoid album where this tag is set with "Various Artists"\&. If a single track within an album is found with AlbumArtists:"Various Artists" the complete album is skipped and won\*(Aqt be queued\&.
+.br
+
+It is planned to allow users to set the values of AlbumArtists tag triggering this behaviour\&. cf\&. feature request #2085 on the tracker\&.
+.RE
+.SH "FILES"
+.PP
+${XDG_CONFIG_HOME}/mpd_sima/mpd_sima\&.cfg
+.RS 4
+Configuration file\&.
+.RE
+.PP
+${XDG_DATA_HOME}/mpd_sima/sima\&.db
+.RS 4
+SQLite DB file\&.
+.RE
+.PP
+${XDG_DATA_HOME}/mpd_sima/WEB_SERVICE/
+.RS 4
+Persistant http cache\&.
+.RE
+.PP
+Usually
+\fBXDG_DATA_HOME\fR
+is set to
+${HOME}/\&.local/share
+and
+\fBXDG_CONFIG_HOME\fR
+to
+${HOME}/\&.config\&.
+.br
+You may override them using command line option
+\fB\-\-var_dir\fR
+(cf\&.
+\fBmpd_sima\fR(1))
+.SH "FEEDBACK/BUGS"
+.PP
+The maintainer would be more than happy to ear from you, don\*(Aqt hesitate to send feedback,
+\m[blue]\fB\%http://kaliko.me/id/\fR\m[]\&.
+.PP
+XMPP
+users are welcome to join the dedicated chat room at
+\m[blue]\fBkaliko\&.me at conf\&.azylum\&.org\fR\m[]\&.
+.SH "SEE ALSO"
+.PP
+\fBmpc\fR(1),
+\fBmpd\fR(1)
+.PP
+/usr/share/doc/mpd\-sima/
+.SH "AUTHOR"
+.PP
+\fBJack Kaliko\fR <\&kaliko at azylum\&.org\&>
+.RS 4
+Wrote this man page and is currently leading MPD_sima project\&.
+.RE
+.SH "COPYRIGHT"
+.br
+Copyright \(co 2009-2014 Jack Kaliko
+.br
+.PP
+This manual page was written for the Debian system (and may be used by others)\&.
+.PP
+Permission is granted to copy, distribute and/or modify this document under the terms of the GNU General Public License, Version 3 published by the Free Software Foundation\&.
+.PP
+On Debian systems, the complete text of the GNU General Public License can be found in
+/usr/share/common\-licenses/GPL\&.
+.sp
diff --git a/data/man/mpd_sima.cfg.5.xml b/data/man/mpd_sima.cfg.5.xml
new file mode 100644
index 0000000..cb6f47d
--- /dev/null
+++ b/data/man/mpd_sima.cfg.5.xml
@@ -0,0 +1,395 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!--
+
+`xsltproc -''-nonet \
+          -''-param man.charmap.use.subset "0" \
+          -''-param make.year.ranges "1" \
+          -''-param make.single.year.ranges "1" \
+          /usr/share/xml/docbook/stylesheet/nwalsh/manpages/docbook.xsl \
+          manpage.xml'
+
+A manual page <package>.<section> will be generated. You may view the
+manual page with: nroff -man <package>.<section> | less'. A typical entry
+in a Makefile or Makefile.am is:
+
+DB2MAN = /usr/share/sgml/docbook/stylesheet/xsl/nwalsh/manpages/docbook.xsl
+XP     = xsltproc -''-nonet -''-param man.charmap.use.subset "0"
+
+manpage.1: manpage.xml
+        $(XP) $(DB2MAN) $<
+
+The xsltproc binary is found in the xsltproc package. The XSL files are in
+docbook-xsl. A description of the parameters you can use can be found in the
+docbook-xsl-doc-* packages. Please remember that if you create the nroff
+version in one of the debian/rules file targets (such as build), you will need
+to include xsltproc and docbook-xsl in your Build-Depends control field.
+Alternatively use the xmlto command/package. That will also automatically
+pull in xsltproc and docbook-xsl.
+
+Notes for using docbook2x: docbook2x-man does not automatically create the
+AUTHOR(S) and COPYRIGHT sections. In this case, please add them manually as
+<refsect1> ... </refsect1>.
+
+To disable the automatic creation of the AUTHOR(S) and COPYRIGHT sections
+read /usr/share/doc/docbook-xsl/doc/manpages/authors.html. This file can be
+found in the docbook-xsl-doc-html package.
+
+Validation can be done using: `xmllint -''-noout -''-valid manpage.xml`
+
+General documentation about man-pages and man-page-formatting:
+man(1), man(7), http://www.tldp.org/HOWTO/Man-Page/
+
+-->
+<!DOCTYPE refentry [
+
+  <!ENTITY dhsection   "5">
+  <!ENTITY dhpackage "mpd-sima">
+  <!ENTITY dhutils "mpd_sima.cfg">
+
+]>
+
+<refentry xmlns="http://docbook.org/ns/docbook"
+          xmlns:xi="http://www.w3.org/2001/XInclude" version="5.0">
+  <xi:include href="info.xml" />
+  <refmeta>
+      <refentrytitle>&dhutils;</refentrytitle>
+      <manvolnum>&dhsection;</manvolnum>
+  </refmeta>
+    <refnamediv>
+        <refname>&dhutils;</refname>
+        <refpurpose>&dhpackage; will try to maintain some titles ahead in your play
+            list following different policies. This manual document the
+            configuration file for &dhpackage;.</refpurpose>
+    </refnamediv>
+    <refsect1 id="description">
+        <title>DESCRIPTION</title>
+        <para>This manual page documents briefly <command>&dhpackage;</command>
+            configuration options available in user configuration file
+            (see <xref linkend="files"/>).</para>
+    </refsect1>
+    <refsect1 id="examples">
+        <title>EXAMPLES</title>
+        <!--
+        <refsect2 id="track">
+            <title>Default queue mode, similar artist.</title>
+            <para></para>
+            <para></para>
+        </refsect2> -->
+        <refsect2 id="album">
+            <title>Album queue mode.</title>
+            <para>Here is an example of album queue configuration.</para>
+            <programlisting><xi:include href="album.cfg" parse="text" />
+            </programlisting>
+        </refsect2>
+    </refsect1>
+
+    <refsect1 id="options">
+        <title>Configuration file</title>
+        <para>The configuration file consists of sections, led by a
+            <command>[section]</command> header and followed by <option>name: value</option>
+            entries, with continuations in the style of RFC 822 (see section
+            3.1.1, “LONG HEADER FIELDS”); <option>name=value</option> is also accepted. Lines
+            beginning with <parameter>'#'</parameter> or <parameter>';'</parameter>
+            are ignored and may be used to provide comments (<emphasis>Nota
+                Bene:</emphasis> inline comment are possible using <parameter>'#'</parameter>).</para>
+        <title>OPTIONS</title>
+        <para>The default values are used in the options lists below.</para>
+        <refsect2 id="MPD">
+            <title>MPD section</title>
+            <para>This section is meant to configure MPD access, MPD host
+                address / port and password if necessary.</para>
+            <variablelist>
+                <!-- Use the variablelist.term.separator and the
+                variablelist.term.break.after parameters to
+                control the term elements. -->
+                <varlistentry> <!-- MPD -->
+                    <term><option>[MPD]</option></term>
+                    <listitem></listitem>
+                </varlistentry>
+                <varlistentry> <!-- MPD.host -->
+                    <term><option>host=</option><replaceable>localhost</replaceable></term>
+                    <listitem>
+                        <para>Set MPD host. Use IP or FQDN.</para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- MPD.port -->
+                    <term><option>port=</option><replaceable>6600</replaceable></term>
+                    <listitem>
+                        <para>Set host port to access MPD to.</para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- MPD.password -->
+                    <term><option>password=</option><replaceable>s3cr3t</replaceable></term>
+                    <listitem>
+                        <para>Set MPD password to use. Do not use this option
+                            if you don't have enabled password protected access
+                            on your MPD server.</para>
+                    </listitem>
+                </varlistentry>
+            </variablelist>
+        </refsect2>
+        <refsect2 id="log">
+            <para>Configure logging.</para>
+            <title>log section</title>
+            <variablelist>
+                <varlistentry> <!-- LOG -->
+                    <term><option>[log]</option></term>
+                    <listitem><para></para></listitem>
+                </varlistentry>
+                <varlistentry> <!-- log.logfile -->
+                    <term><option>logfile=</option></term>
+                    <listitem>
+                        <para>File to log to, usually in dæmon mode.<sbr />Defaut
+                            (empty or unset) is to log to stdin/stdout.</para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- log.verbosity -->
+                    <term><option>verbosity=</option><replaceable>info</replaceable></term>
+                    <listitem>
+                        <para>Logging verbosity among
+                            <replaceable>debug</replaceable>,
+                            <replaceable>info</replaceable>,
+                            <replaceable>warning</replaceable>,
+                            <replaceable>error</replaceable>.</para>
+                    </listitem>
+                </varlistentry>
+            </variablelist>
+        </refsect2>
+        <refsect2 id="sima">
+            <title>sima section</title>
+            <para>This section allows you to tweak core &dhutils; configuration.</para>
+            <variablelist>
+                <varlistentry> <!-- SIMA -->
+                    <term><option>[sima]</option></term>
+                    <listitem><para></para></listitem>
+                </varlistentry>
+                <varlistentry> <!-- sima.history_duration -->
+                    <term><option>history_duration=</option><replaceable>8</replaceable></term>
+                    <listitem>
+                        <para>How far to look back in history to avoid to play
+                            twice the same track/title (duration in
+                            hours).</para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- sima.queue_length -->
+                    <term><option>queue_length=</option><replaceable>1</replaceable></term>
+                    <listitem>
+                        <para>This value triggers queue process if the play
+                            list length is less than specified
+                            queue_length.</para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- sima.user_db -->
+                    <term><option>user_db=</option><replaceable>false</replaceable></term>
+                    <listitem>
+                        <para>Temporarily removed feature</para>
+                        <!--<para>Look for user defined similarities in user data base.</para>-->
+                    </listitem>
+                </varlistentry>
+            </variablelist>
+            <para>&dhpackage;'s plugin management for internal source plugin
+                and contrib (ie. external plugins).<sbr /> Plugins list is a
+                comma separated string list.<sbr /> Optional plugin's
+                configuration lays in its own section.<sbr />For instance a
+                "AwesomePlugin" declared here gets its configuration from the
+                corresponding section "[awesomeplugin]".<sbr />internal plugins
+                will look for a section named after the lower-cased name of the
+                pluglin, ie.  RandomFallBack → randomfallback.
+            </para>
+            <variablelist>
+                <varlistentry> <!-- sima.internal -->
+                    <term><option>internal=</option><replaceable>Crop, RandomFallBack, Lastfm</replaceable></term>
+                    <listitem>
+                        <para><option>Crop</option> and <option>RandomFallback</option>
+                            are utilities plugins while <option>Lastfm</option> is the
+                            actual queue plugin.<sbr /> Another queue plugin is available as
+                            a "techno preview", it relies on EchoNest web services, replace
+                            <option>LastFm</option> with <option>EchoNest</option> to try.</para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- sima.contrib -->
+                    <term><option>contrib=</option><replaceable></replaceable></term>
+                    <listitem>
+                        <para></para>
+                    </listitem>
+                </varlistentry>
+            </variablelist>
+        </refsect2>
+        <refsect2 id="crop">
+            <title>Crop section</title>
+            <para>crop plugin's configuration:</para>
+                <varlistentry> <!-- crop -->
+                    <term><option>[crop]</option></term>
+                </varlistentry>
+                <varlistentry> <!-- crop.consume -->
+                    <term><option>consume=</option><replaceable>0</replaceable></term>
+                    <listitem>
+                        <para>How many played tracks to keep in the play list.
+                            Allow to maintain a fixed length play list.
+                            Set to 0 to keep all played tracks.
+                        </para>
+                    </listitem>
+                </varlistentry>
+        </refsect2>
+        <refsect2 id="randomfallback">
+            <title>RandomFallback section</title>
+            <para>RandomFallback plugin's configuration:</para>
+                <varlistentry> <!-- randomfallback -->
+                    <term><option>[randomfallback]</option></term>
+                </varlistentry>
+                <varlistentry> <!-- randomfallback.flavour -->
+                    <term><option>flavour=</option><replaceable>sensible</replaceable></term>
+                    <listitem>
+                        <para>When no similar tracks are found, falling back to
+                            random queuing. Different mode, aka random flavour,
+                            are available:
+                            <replaceable>pure</replaceable>,
+                            <replaceable>sensible</replaceable>,
+                            <replaceable>genre</replaceable>.
+                            <itemizedlist mark='bullet'>
+                                <listitem>
+                                    <para><replaceable>pure</replaceable>, pure random choice, even among recently played track.
+                                    </para>
+                                </listitem>
+                                <listitem >
+                                    <para><replaceable>sensible</replaceable>, use play history to filter chosen tracks.
+                                    </para>
+                                </listitem>
+                                <listitem>
+                                    <para><replaceable>genre</replaceable>, Not implemented yet.
+                                    </para>
+                                </listitem>
+                            </itemizedlist>
+                        </para>
+                    </listitem>
+                </varlistentry>
+        </refsect2>
+        <refsect2 id="lastfm">
+            <title>LastFm section</title>
+            <para>This section allows you to tweak LastFM plugin's configuration.</para>
+            <variablelist>
+                <varlistentry> <!-- lastfm -->
+                    <term><option>[lastfm]</option></term>
+                </varlistentry>
+                <varlistentry> <!-- lastfm.queue_mode -->
+                    <term><option>queue_mode=</option><replaceable>track</replaceable></term>
+                    <listitem>
+                        <para>Queue mode to use among
+                            <replaceable>track</replaceable>,
+                            <replaceable>top</replaceable> and
+                            <replaceable>album</replaceable> (see <xref linkend="queue_mode"/> for info about queue modes).</para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- lastfm.max_art -->
+                    <term><option>max_art=</option><replaceable>10</replaceable></term>
+                    <listitem>
+                        <para>Number of similar artist to retrieve from local
+                            media library.<sbr />When set to something superior
+                            to zero, it tries to get as much similar artists
+                            from media library.</para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- lastfm.depth -->
+                    <term><option>depth=</option><replaceable>1</replaceable></term>
+                    <listitem>
+                        <para>How many artists to base on similar artists
+                            search.<sbr /> The first is the last played artist
+                            and so on back in the history. Highter depth
+                            allows to get wider suggestions, it might help to
+                            reduce looping over same artists.
+                            </para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- lastfm.single_album -->
+                    <term><option>single_album=</option><replaceable>false</replaceable></term>
+                    <listitem>
+                        <para>Prevent from queueing a track from the same album
+                            (it often happens with OST).<sbr />
+                            Only relevant in "track" queue mode.</para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- lastfm.track_to_add -->
+                    <term><option>track_to_add=</option><replaceable>1</replaceable></term>
+                    <listitem>
+                        <para>How many track(s) to add. Only relevant in
+                            <option>top</option> and <option>track</option>
+                            queue modes.</para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- lastfm.album_to_add -->
+                    <term><option>album_to_add=</option><replaceable>1</replaceable></term>
+                    <listitem>
+                        <para>How many album(s) to add. Only relevant in
+                            <option>album</option> queue modes.</para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry> <!-- lastfm.cache -->
+                    <term><option>cache=</option><replaceable>True</replaceable></term>
+                    <listitem>
+                        <para>Whether or not to use on-disk persistent http
+                            cache.<sbr />When set to "true", sima will use a
+                            persistent cache for its http client. The cache is
+                            written along with the dbfile in:<sbr />
+                            <filename>$XDG_CONFIG_HOME/mpd_sima/http/WEB_SERVICE</filename>.<sbr/>
+                            If set to "false", caching is still done but in memory.
+                        </para>
+                    </listitem>
+                </varlistentry>
+            </variablelist>
+        </refsect2>
+    </refsect1>
+    <refsect1 id="queue_mode">
+        <title>QUEUE MODES</title>
+        <para>&dhpackage; offers different queue modes. All of them pick up
+            tracks from artists similar to the one currently played.</para>
+        <para>&dhpackage; tries preferably to chose among unplayed artists or
+            at least not recently played artist. Concerning track and album
+            queue modes titles are chosen purely at random among unplayed
+            tracks.</para>
+        <variablelist>
+            <varlistentry>
+                <term><option>track</option></term>
+                <listitem>
+                    <para>Queue a similar track chosen at random from a similar artist.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry>
+                <term><option>top</option></term>
+                <listitem>
+                    <para>Queue a track from a similar artist, chosen among
+                        "top tracks" according to last.fm data mining.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry>
+                <term><option>album</option></term>
+                <listitem>
+                    <para>Queue a whole album chosen at random from a similar artist.</para>
+                    <para><emphasis>Nota Bene:</emphasis><sbr /> Due to the
+                        track point of view of database build upon tracks tags
+                        an album lookup for a specific artist will return
+                        albums as soon as this artist appears in a single track
+                        of the album.<sbr />
+                        For instance looking for album from "The Velvet
+                        Underground" will fetch "Last Days" and "Juno" OSTs
+                        because the band appears on the soundtrack of these two
+                        movies.<sbr />
+                        A solution is for you to set AlbumArtists tag to
+                        something different than the actual artist of the
+                        track. For compilations, OSTs etc. a strong convention
+                        is to use "Various Artists" for this tag.</para>
+                    <para>&dhpackage; is currently looking for AlbumArtists tags
+                        and avoid album where this tag is set with "Various
+                        Artists". If a single track within an album is found
+                        with AlbumArtists:"Various Artists" the complete album
+                        is skipped and won't be queued.<sbr />
+                        It is planned to allow users to set the values of
+                        AlbumArtists tag triggering this behaviour.  cf.
+                        feature request #2085 on the tracker.</para>
+                </listitem>
+            </varlistentry>
+        </variablelist>
+    </refsect1>
+  <xi:include href="files.xml" />
+  <xi:include href="feedback.xml" />
+  <xi:include href="seealso.xml" />
+</refentry>
diff --git a/data/man/seealso.xml b/data/man/seealso.xml
new file mode 100644
index 0000000..79415a1
--- /dev/null
+++ b/data/man/seealso.xml
@@ -0,0 +1,17 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<refsect1 id="see_also">
+    <title>SEE ALSO</title>
+    <!-- In alpabetical order. -->
+    <para><citerefentry>
+            <refentrytitle>mpc</refentrytitle>
+            <manvolnum>1</manvolnum>
+            </citerefentry>, <citerefentry>
+            <refentrytitle>mpd</refentrytitle>
+            <manvolnum>1</manvolnum>
+    </citerefentry></para>
+    <para>
+        <filename>/usr/share/doc/mpd-sima/</filename>
+    </para>
+</refsect1>
+<!-- vim:filetype=docbk
+-->
diff --git a/data/man/simadb_cli.1 b/data/man/simadb_cli.1
new file mode 100644
index 0000000..b221a72
--- /dev/null
+++ b/data/man/simadb_cli.1
@@ -0,0 +1,383 @@
+'\" t
+.\"     Title: simadb_cli
+.\"    Author: Jack Kaliko <kaliko at azylum.org>
+.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
+.\"      Date: 06/10/2014
+.\"    Manual: mpd-sima 0.12.0 User Manual
+.\"    Source: mpd-sima
+.\"  Language: English
+.\"
+.TH "SIMADB_CLI" "1" "06/10/2014" "mpd-sima" "mpd-sima 0.12.0 User Manual"
+.\" -----------------------------------------------------------------
+.\" * Define some portability stuff
+.\" -----------------------------------------------------------------
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.\" http://bugs.debian.org/507673
+.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.ie \n(.g .ds Aq \(aq
+.el       .ds Aq '
+.\" -----------------------------------------------------------------
+.\" * set default formatting
+.\" -----------------------------------------------------------------
+.\" disable hyphenation
+.nh
+.\" disable justification (adjust text to left margin only)
+.ad l
+.\" -----------------------------------------------------------------
+.\" * MAIN CONTENT STARTS HERE *
+.\" -----------------------------------------------------------------
+.SH "NAME"
+simadb_cli \- simadb_cli is a command line interface editor for the sima user DB\&.
+.SH "SYNOPSIS"
+.HP \w'\fBsimadb_cli\fR\ 'u
+\fBsimadb_cli\fR \fB\-\-add_similarity=\fR\fIsimilarity_string\fR [\fB\-\-check_names\fR] [\fB\-\-dbfile=\fR\fIdb_file\fR] [\fB\-\-reciprocal\fR] [\fB\-\-host=\fR\fImpd_host\fR] [\fB\-\-port=\fR\fImpd_port\fR]
+.HP \w'\fBsimadb_cli\fR\ 'u
+\fBsimadb_cli\fR \fB\-\-remove_artist=\fR\fIartist\fR [\fB\-\-dbfile=\fR\fIdb_file\fR] [\fB\-\-reciprocal\fR]
+.HP \w'\fBsimadb_cli\fR\ 'u
+\fBsimadb_cli\fR \fB\-\-remove_similarity=\fR\fI"main\ artist,similar\ artist"\fR [\fB\-\-dbfile=\fR\fIdb_file\fR] [\fB\-\-reciprocal\fR]
+.HP \w'\fBsimadb_cli\fR\ 'u
+\fBsimadb_cli\fR \fB\-\-purge_hist\fR [\fB\-\-dbfile=\fR\fIdb_file\fR]
+.HP \w'\fBsimadb_cli\fR\ 'u
+\fBsimadb_cli\fR \fB\-\-view_artist=\fR\fB\fI"artist\ name"\fR\fR [\fB\-\-dbfile=\fR\fIdb_file\fR]
+.HP \w'\fBsimadb_cli\fR\ 'u
+\fBsimadb_cli\fR \fB\-\-view_all\fR [\fB\-\-dbfile=\fR\fIdb_file\fR]
+.HP \w'\fBsimadb_cli\fR\ 'u
+\fBsimadb_cli\fR {\fB\-\-bl_curr_trk\fR | \fB\-\-bl_curr_art\fR | \fB\-\-bl_curr_alb\fR | \fB\-\-bl_art=\fR\fIartist_name\fR} [\fB\-\-dbfile=\fR\fIdb_file\fR] [\fB\-\-host=\fR\fImpd_host\fR] [\fB\-\-port=\fR\fImpd_port\fR]
+.HP \w'\fBsimadb_cli\fR\ 'u
+\fBsimadb_cli\fR \fB\-\-remove_bl=\fR\fIrow_id\fR [\fB\-\-dbfile=\fR\fIdb_file\fR]
+.HP \w'\fBsimadb_cli\fR\ 'u
+\fBsimadb_cli\fR \fB\-\-view_bl\fR [\fB\-\-dbfile=\fR\fIdb_file\fR]
+.HP \w'\fBsimadb_cli\fR\ 'u
+\fBsimadb_cli\fR {{\fB\-h\fR\ |\ \fB\-\-help\fR} | \fB\-\-version\fR}
+.SH "DESCRIPTION"
+.PP
+This manual page documents briefly the
+\fBsimadb_cli\fR
+commands\&.
+.PP
+simadb_cli is a command line interface to get and edit users similarities and blacklist database used with MPD_sima\&. The default database file (see
+the section called \(lqFILES\(rq) can be overridden if you want\&.
+.PP
+Consider reading
+the section called \(lqA WORD ABOUT SIMA DATA BASE\(rq
+to understand the structure and relation of similarities within the database\&.
+.SH "EXAMPLE"
+.SS "Similarity edition"
+.PP
+Here follows some simple examples on how to deal with similarity database\&.
+.PP
+Pay attention, the following examples set one\-way similarities in the DB! Read more about it in
+the section called \(lqA WORD ABOUT SIMA DATA BASE\(rq\&.
+.PP
+\fIAdding a similarity between two artists\&.\fR
+In the following example "Pelican" will point to "Russian Circles" with a match score of 88% (ie\&. "Russian Circles" 88% similar to Pelican, not reciprocal), it will also check against MPD the presence of both artists in the music library\&.
+.PP
+\fBsimadb_cli \-\-add "Pelican,Russian Circles:80" \-\-check_names\fR
+.PP
+Similarity string use comma "," as artists separator and semi colon ":" for artist/similarity score separator, cf\&.
+the section called \(lqSIMILARITY FORMAT\(rq\&.
+.PP
+\fIAdding a similarity between multiple artists\&.\fR
+In the following example "Rage Against The Machine" will point to "Tool" and "Audioslave" as similar artists and controls artists names are actually in MPD music library\&.
+.PP
+\fBsimadb_cli \-\-add "Rage Against The Machine,Tool:70,Audiosalve:80" \-\-check_names\fR
+.PP
+\fIViewing similarit(y|ies) for an artist\&.\fR
+In the following example we are looking for entries for "Rage Against The Machine"
+.PP
+\fBsimadb_cli \-\-view_artist "Rage Against The Machine"\fR
+.SS "Black list edition"
+.PP
+\fIAdding to black list\&.\fR
+You can add a single track, an album or an artist to the black list\&. The element to black list is chosen from the currently playing track\&. Use
+\fB\-\-bl_curr_trk\fR
+to prevent simadb_cli to queue this track,
+\fB\-\-bl_curr_alb\fR
+or
+\fB\-\-bl_curr_art\fR
+respectively for the album and the artist\&.
+.PP
+Remember you need access to your MPD server to retrieve information to black list\&. Defaults are localhost:6600 or found in environment variables but you may set it up from command line:
+.PP
+\fBsimadb_cli \-\-bl_curr_art \-S mympd\&.example\&.org\fR
+.PP
+\fITo black list a specific artist\fR
+(not currently playing) you can use
+\fB\-\-bl_ar="Artist name to black list"\fR\&.
+.SH "OPTIONS"
+.PP
+The program follows the usual GNU command line syntax, with long options starting with two dashes ("\-")\&. A summary of options is included below\&.
+.PP
+\fB\-h\fR, \fB\-\-help\fR
+.RS 4
+Print help and exit\&.
+.RE
+.PP
+\fB\-\-version\fR
+.RS 4
+Print version and exit\&.
+.RE
+.PP
+\fB\-a \fR\fB\fIsimilarity_string\fR\fR, \fB\-\-add_similarity=\fR\fB\fIsimilarity_string\fR\fR
+.RS 4
+Add similarity to the database\&.
+.br
+For more details about the
+\fIsimilarity_string\fR
+see
+the section called \(lqSIMILARITY FORMAT\(rq\&.
+.RE
+.PP
+\fB\-c\fR, \fB\-\-check_names\fR
+.RS 4
+Use with
+\fB\-\-add_similarity\fR
+in order to check artists names used in
+\fIsimilarity_string\fR\&. simadb_cli will control presence of artists names in MPD library\&. Default is to look for MPD server on localhost:6600 or environment variables
+\fBMPD_HOST\fR
+and
+\fBMPD_PORT\fR
+if set\&.
+.br
+You can as well give simadb_cli host/port on the command line using respectively
+\fB\-S\fR
+and
+\fB\-P\fR\&.
+.RE
+.PP
+\fB\-\-bl_art=\fR\fB\fIartist_name\fR\fR
+.RS 4
+Use to black list
+\fIartist_name\fR\&. simadb_cli is checking
+\fIartist_name\fR
+is actually in MPD music library (cf
+\fB\-S\fR
+and
+\fB\-P\fR
+options to set MPD host/address if necessary)\&.
+.sp
+If
+\fIartist_name\fR
+is not found the script print out a list of matching artists\&.
+.RE
+.PP
+\fB\-\-bl_curr_trk\fR | \fB\-\-bl_curr_art\fR | \fB\-\-bl_curr_alb\fR
+.RS 4
+Use to black list the currently playing track|artist|album\&. You need access to your MPD server, use
+\fB\-S\fR
+and
+\fB\-P\fR
+to set MPD host/address if necessary\&.
+.RE
+.PP
+\fB\-d \fR\fB\fIdb_file\fR\fR, \fB\-\-dbfile=\fR\fB\fIdb_file\fR\fR
+.RS 4
+Use the specific file
+\fIdb_file\fR
+as database\&.
+.br
+Default is too use
+\fBXDG_DATA_HOME\fR
+(see
+the section called \(lqFILES\(rq)\&.
+.RE
+.PP
+\fB\-\-purge_hist\fR
+.RS 4
+Purge history, you may supply an alternative DB file with \-\-dbfile option\&.
+.RE
+.PP
+\fB\-r\fR, \fB\-\-reciprocal\fR
+.RS 4
+Use with an editing options in order to edit reciprocal similarity as well\&.
+\fB\-\-add_similarity\fR
+and
+\fB\-\-remove_{artist|similarity}\fR
+are supporting reciprocal edition\&.
+.sp
+\fIN\&.B\fR: this option has to appear after the editing option on the command line\&.
+.sp
+See
+the section called \(lqA WORD ABOUT SIMA DATA BASE\(rq
+for further information about reciprocity notion\&.
+.RE
+.PP
+\fB\-\-remove_artist=\fR\fB\fIartist\fR\fR
+.RS 4
+Use to remove an artist entry (as main artist) with its associated similarities\&. To remove artist where it appears as a similar artist use the
+\fB\-\-reciprocal\fR
+option\&.
+.RE
+.PP
+\fB\-\-remove_bl=\fR\fB\fIrow_id\fR\fR
+.RS 4
+Use to remove a black list entry\&. To get the row_id to suppress use
+\fB\-\-view_bl\fR
+option\&.
+.RE
+.PP
+\fB\-\-remove_similarity=\fR\fB\fI"main artist,similar artist"\fR\fR
+.RS 4
+Use to remove a single similarity between a main artist and an associated similarity\&. Give the main artist first, use comma (",") to separate it from similar artist\&.
+.br
+Use of
+\fB\-\-reciprocal\fR
+is possible here, see
+the section called \(lqA WORD ABOUT SIMA DATA BASE\(rq\&.
+.sp
+This option is useful in case you want to remove only a specific similarity between two artists, to remove completely an artist use
+\fB\-\-remove_artist\fR
+instead\&.
+.RE
+.PP
+\fB\-v \fR\fB\fI"artist name"\fR\fR, \fB\-\-view_artist=\fR\fB\fI"artist name"\fR\fR
+.RS 4
+Get entries for
+\fI"artist name"\fR
+in the data base (print to stdout)\&.
+.RE
+.PP
+\fB\-\-view_bl\fR
+.RS 4
+Get all entries in the black list\&.
+.RE
+.PP
+\fB\-\-view_all\fR
+.RS 4
+Get all entries in the data base (print to stdout)\&.
+.RE
+.PP
+\fB\-P \fR\fB\fImpd_port\fR\fR, \fB\-\-port=\fR\fB\fImpd_port\fR\fR
+.RS 4
+Use the specific port number
+\fImpd_port\fR
+on MPD server\&. This overrides
+\fBMPD_PORT\fR
+environment variable\&.
+.br
+Default is
+\fI6600\fR\&.
+.RE
+.PP
+\fB\-S \fR\fB\fImpd_host\fR\fR, \fB\-\-host=\fR\fB\fImpd_host\fR\fR
+.RS 4
+Use the specific host
+\fImpd_host\fR
+as MPD server\&.
+.br
+\fImpd_host\fR
+can be an
+IP
+or a fully qualified domain name as long as your system can resolve it\&. This overrides
+\fBMPD_HOST\fR
+environment variable\&.
+.br
+Default is
+\fIlocalhost\fR\&.
+.RE
+.SH "FILES"
+.PP
+${XDG_DATA_HOME}/mpd_sima/sima\&.db
+.RS 4
+SQLite DB file\&. Usually
+\fBXDG_DATA_HOME\fR
+is set to
+${HOME}/\&.local/share\&.
+.RE
+.SH "SIMILARITY FORMAT"
+.PP
+The
+\fIsimilarity_string\fR
+has to be formatted following a special pattern in order for simadb_cli to extract similarity relations between artists names\&. Usually a similarity entry is defined as a main artist, lets say
+\fImain_art\fR, followed by a list of similar artists which you want to be related to that
+\fImain_art\fR, each artist of that list with a specific similarity value, a match score, quantifying the similarity relation with the
+\fImain_art\fR\&. The match score value is an integer in [0 ,100] with 100 corresponding to a perfect match\&.
+.PP
+\fIsimilarity_string\fR
+is then to be formatted as follow:
+.PP
+\fBmain_art,first artist:<score>,second artist:<score>\fR
+.PP
+Each artist group are separated with commas (",") and inside each group the artist name and the match score is colon (":") separated\&. Obviously the first artist group, as the main artist, does not have a match score\&.
+.PP
+Lets see how it works with an example\&. I consider "Led Zeppelin" to be similar to "Tool" with a match score of 25, I also want to have "Audioslave" related to "Led Zeppelin" with a score of 20\&. Then the
+\fIsimilarity_string\fR
+will be the following:
+.PP
+\fBLed Zeppelin,Tool:25,Audiosalve:20\fR
+.PP
+See
+the section called \(lqA WORD ABOUT SIMA DATA BASE\(rq
+for more details about how similarities are handled
+.SH "A WORD ABOUT SIMA DATA BASE"
+.PP
+The similarity database is defined from the point of view of a
+\fImain artist\fR
+which is declared related to a list of
+\fIsimilar artists\fR\&. That means when you define
+\fImain_art\fR
+to be similar to
+\fIsim_art A\fR
+and
+\fIsim_art B\fR
+the reciprocal won\*(Aqt be true,
+\fIsim_art A\fR
+and
+\fIsim_art B\fR
+are not similar to
+\fImain_art\fR\&. At least this is the default behavior when you edit entries with simadb_cli, this is also the way last\&.fm is working concerning similar artists\&. This documentation is using that particular terminology to specify which kind of artist we are dealing with : "main artist" or "similar artist"\&.
+.br
+The
+\fB\-\-reciprocal\fR
+option allows one to add reciprocal relation where
+\fIsim_art A\fR
+and
+\fIsim_art B\fR
+become respectively the
+\fImain_art\fR\&. Using
+\fB\-\-reciprocal\fR
+you will then edit two more entries in the database\&. To summarize here is what you\*(Aqll end up with in your data base adding similarity with this string
+\fBmain_art,sim_art A:34,sim_art B:45\fR\&.
+.PP
+\fBsimadb_cli \-\-reciprocal \-\-add_similarity=main_art,sim_art A:34,sim_art B:45\fR
+.PP
+main_art similar to sim_art A:34 and sim_art B:45
+.br
+sim_art A similar to main_art:34
+.br
+sim_art B similar to main_art:45
+.PP
+Without the reciprocal option you would have add only the first similarity\&. Usually using the reciprocal option is the desired behavior, at least what users have in mind when thinking of similarity relation between to artists but keep in mind that it may lead to have MPD_sima more sensible to loop over the same two artist (ASSERTION TO BE CONFIRMED)\&.
+.SH "FEEDBACK/BUGS"
+.PP
+The maintainer would be more than happy to ear from you, don\*(Aqt hesitate to send feedback,
+\m[blue]\fB\%http://kaliko.me/id/\fR\m[]\&.
+.PP
+XMPP
+users are welcome to join the dedicated chat room at
+\m[blue]\fBkaliko\&.me at conf\&.azylum\&.org\fR\m[]\&.
+.SH "SEE ALSO"
+.PP
+\fBmpc\fR(1),
+\fBmpd\fR(1)
+.PP
+/usr/share/doc/mpd\-sima/
+.SH "AUTHOR"
+.PP
+\fBJack Kaliko\fR <\&kaliko at azylum\&.org\&>
+.RS 4
+Wrote this man page and is currently leading MPD_sima project\&.
+.RE
+.SH "COPYRIGHT"
+.br
+Copyright \(co 2009-2014 Jack Kaliko
+.br
+.PP
+This manual page was written for the Debian system (and may be used by others)\&.
+.PP
+Permission is granted to copy, distribute and/or modify this document under the terms of the GNU General Public License, Version 3 published by the Free Software Foundation\&.
+.PP
+On Debian systems, the complete text of the GNU General Public License can be found in
+/usr/share/common\-licenses/GPL\&.
+.sp
diff --git a/data/man/simadb_cli.1.xml b/data/man/simadb_cli.1.xml
new file mode 100644
index 0000000..5448f86
--- /dev/null
+++ b/data/man/simadb_cli.1.xml
@@ -0,0 +1,409 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!--
+
+`xsltproc -''-nonet \
+          -''-param man.charmap.use.subset "0" \
+          -''-param make.year.ranges "1" \
+          -''-param make.single.year.ranges "1" \
+          /usr/share/xml/docbook/stylesheet/nwalsh/manpages/docbook.xsl \
+          manpage.xml'
+
+A manual page <package>.<section> will be generated. You may view the
+manual page with: nroff -man <package>.<section> | less'. A typical entry
+in a Makefile or Makefile.am is:
+
+DB2MAN = /usr/share/sgml/docbook/stylesheet/xsl/nwalsh/manpages/docbook.xsl
+XP     = xsltproc -''-nonet -''-param man.charmap.use.subset "0"
+
+manpage.1: manpage.xml
+        $(XP) $(DB2MAN) $<
+
+The xsltproc binary is found in the xsltproc package. The XSL files are in
+docbook-xsl. A description of the parameters you can use can be found in the
+docbook-xsl-doc-* packages. Please remember that if you create the nroff
+version in one of the debian/rules file targets (such as build), you will need
+to include xsltproc and docbook-xsl in your Build-Depends control field.
+Alternatively use the xmlto command/package. That will also automatically
+pull in xsltproc and docbook-xsl.
+
+Notes for using docbook2x: docbook2x-man does not automatically create the
+AUTHOR(S) and COPYRIGHT sections. In this case, please add them manually as
+<refsect1> ... </refsect1>.
+
+To disable the automatic creation of the AUTHOR(S) and COPYRIGHT sections
+read /usr/share/doc/docbook-xsl/doc/manpages/authors.html. This file can be
+found in the docbook-xsl-doc-html package.
+
+Validation can be done using: `xmllint -''-noout -''-valid manpage.xml`
+
+General documentation about man-pages and man-page-formatting:
+man(1), man(7), http://www.tldp.org/HOWTO/Man-Page/
+
+-->
+<!DOCTYPE refentry [
+
+  <!ENTITY dhsection   "1">
+  <!ENTITY dhpackage "mpd-sima">
+  <!ENTITY dhutils "simadb_cli">
+
+]>
+
+<refentry xmlns="http://docbook.org/ns/docbook"
+          xmlns:xi="http://www.w3.org/2001/XInclude" version="5.0">
+  <xi:include href="info.xml" />
+  <refmeta>
+      <refentrytitle>&dhutils;</refentrytitle>
+      <manvolnum>&dhsection;</manvolnum>
+  </refmeta>
+    <refnamediv>
+        <refname>&dhutils;</refname>
+        <refpurpose>&dhutils; is a command line interface editor for the sima user DB.</refpurpose>
+    </refnamediv>
+    <refsynopsisdiv>
+        <cmdsynopsis><!-- REGULAR EDIT (ADD) OPTIONS -->
+            <command>&dhutils;</command>
+            <arg choice="plain"><option>--add_similarity=</option><replaceable class="option">similarity_string</replaceable></arg>
+            <arg choice="opt">
+                <option>--check_names</option>
+            </arg>
+            <arg choice="opt">
+                <option>--dbfile=</option><replaceable class="option">db_file</replaceable>
+            </arg>
+            <arg choice="opt">
+                <option>--reciprocal</option>
+            </arg>
+            <arg choice="opt">
+                <option>--host=</option><replaceable class="option">mpd_host</replaceable>
+            </arg>
+            <arg choice="opt">
+                <option>--port=</option><replaceable class="option">mpd_port</replaceable>
+            </arg>
+        </cmdsynopsis>
+        <cmdsynopsis><!-- EDIT (RM ARTIST) OPTIONS -->
+            <command>&dhutils;</command>
+            <arg choice="plain"><option>--remove_artist=</option><replaceable class="parameter">artist</replaceable></arg>
+            <arg choice="opt">
+                <option>--dbfile=</option><replaceable class="option">db_file</replaceable>
+            </arg>
+            <arg choice="opt">
+                <option>--reciprocal</option>
+            </arg>
+        </cmdsynopsis>
+        <cmdsynopsis><!-- EDIT (RM SIM) OPTIONS -->
+            <command>&dhutils;</command>
+            <arg choice="plain"><option>--remove_similarity=</option><replaceable class="parameter">"main artist,similar artist"</replaceable></arg>
+            <arg choice="opt">
+                <option>--dbfile=</option><replaceable class="option">db_file</replaceable>
+            </arg>
+            <arg choice="opt">
+                <option>--reciprocal</option>
+            </arg>
+        </cmdsynopsis>
+        <cmdsynopsis><!-- EDIT (PURGE HIST) OPTIONS -->
+            <command>&dhutils;</command>
+            <arg choice="plain"><option>--purge_hist</option></arg>
+            <arg choice="opt">
+                <option>--dbfile=</option><replaceable class="option">db_file</replaceable>
+            </arg>
+        </cmdsynopsis>
+        <cmdsynopsis><!-- REGULAR VIEW OPTIONS -->
+            <command>&dhutils;</command>
+            <arg choice="plain"><option>--view_artist=<replaceable class="parameter">"artist name"</replaceable></option></arg>
+            <arg choice="opt">
+                <option>--dbfile=</option><replaceable class="option">db_file</replaceable>
+            </arg>
+        </cmdsynopsis>
+        <cmdsynopsis><!-- VIEW ALL ENTRIES OPTIONS -->
+            <command>&dhutils;</command>
+            <arg choice="plain"><option>--view_all</option></arg>
+            <arg choice="opt">
+                <option>--dbfile=</option><replaceable class="option">db_file</replaceable>
+            </arg>
+        </cmdsynopsis>
+        <cmdsynopsis><!-- EDIT (BLACK LIST) OPTIONS -->
+            <command>&dhutils;</command>
+            <group choice="req">
+                <arg choice="plain"><option>--bl_curr_trk</option></arg>
+                <arg choice="plain"><option>--bl_curr_art</option></arg>
+                <arg choice="plain"><option>--bl_curr_alb</option></arg>
+                <arg choice="plain"><option>--bl_art=</option><replaceable class="parameter">artist_name</replaceable></arg>
+            </group>
+            <arg choice="opt">
+                <arg choice="plain"><option>--dbfile=</option><replaceable class="parameter">db_file</replaceable></arg>
+            </arg>
+            <arg choice="opt">
+                <arg choice="plain"><option>--host=</option><replaceable class="option">mpd_host</replaceable></arg>
+            </arg>
+            <arg choice="opt">
+                <arg choice="plain"><option>--port=</option><replaceable class="option">mpd_port</replaceable></arg>
+            </arg>
+        </cmdsynopsis>
+        <cmdsynopsis><!-- EDIT (RM BL) OPTIONS -->
+            <command>&dhutils;</command>
+            <arg choice="plain"><option>--remove_bl=</option><replaceable class="parameter">row_id</replaceable></arg>
+            <arg choice="opt">
+                <option>--dbfile=</option><replaceable class="option">db_file</replaceable>
+            </arg>
+        </cmdsynopsis>
+        <cmdsynopsis><!-- VIEW BL OPTIONS -->
+            <command>&dhutils;</command>
+            <arg choice="plain"><option>--view_bl</option></arg>
+            <arg choice="opt">
+                <option>--dbfile=</option><replaceable class="option">db_file</replaceable>
+            </arg>
+        </cmdsynopsis>
+        <cmdsynopsis><!-- HELP/VERSION -->
+            <command>&dhutils;</command>
+            <!-- Normally the help and version options make the programs stop
+            right after outputting the requested information. -->
+            <group choice="req">
+                <arg choice="plain">
+                    <group choice="req">
+                        <arg choice="plain"><option>-h</option></arg>
+                        <arg choice="plain"><option>--help</option></arg>
+                    </group>
+                </arg>
+                <arg choice="plain"><option>--version</option></arg>
+            </group>
+        </cmdsynopsis>
+    </refsynopsisdiv>
+    <refsect1 id="description">
+        <title>DESCRIPTION</title>
+        <para>This manual page documents briefly the
+            <command>&dhutils;</command> commands.</para>
+        <para>simadb_cli is a command line interface to get and edit users
+            similarities and blacklist database used with MPD_sima. The default
+            database file (see <xref linkend="files"/>) can be overridden if
+            you want.</para>
+        <para>Consider reading <xref linkend="simadb" /> to understand the
+            structure and relation of similarities within the database.</para>
+    </refsect1>
+    <refsect1 id="example">
+        <title>EXAMPLE</title>
+        <refsect2 id="similarity">
+            <title>Similarity edition</title>
+            <para>Here follows some simple examples on how to deal with similarity database.</para>
+            <para>Pay attention, the following examples set one-way similarities in the DB! Read more about it in <xref linkend="simadb" />.</para>
+            <para><emphasis>Adding a similarity between two artists.</emphasis> In the following example "Pelican" will point 
+                to "Russian Circles" with a match score of 88% (ie. "Russian Circles"
+                88% similar to Pelican, not reciprocal), it will also check against MPD
+                the presence of both artists in the music library.</para>
+            <para><command>&dhutils; --add "Pelican,Russian Circles:80" --check_names</command></para>
+            <para>Similarity string use comma "," as artists separator and semi colon ":" for
+                artist/similarity score separator, cf. <xref linkend="simiformat"/>.</para>
+            <para><emphasis>Adding a similarity between multiple artists.</emphasis> In the following example "Rage
+                Against The Machine" will point to "Tool" and "Audioslave" as similar
+                artists and controls artists names are actually in MPD music library.</para>
+            <para><command>&dhutils; --add "Rage Against The Machine,Tool:70,Audiosalve:80" --check_names</command></para>
+            <para><emphasis>Viewing similarit(y|ies) for an artist.</emphasis> In the
+                following example we are looking for entries for "Rage Against The Machine"</para>
+            <para><command>&dhutils; --view_artist "Rage Against The Machine"</command></para>
+        </refsect2>
+        <refsect2 id="blacklist">
+            <title>Black list edition</title>
+            <para><emphasis>Adding to black list.</emphasis> You can add a single
+                track, an album or an artist to the black list. The element to
+                black list is chosen from the currently playing track. Use
+                <option>--bl_curr_trk</option> to prevent &dhutils; to queue this
+                track, <option>--bl_curr_alb</option> or <option>--bl_curr_art</option> respectively for the album and the
+                artist.
+            </para>
+            <para>Remember you need access to your MPD server to retrieve
+                information to black list. Defaults are localhost:6600 or found in
+                environment variables but you may set it up from command
+                line:
+            </para>
+            <para><command>&dhutils; --bl_curr_art -S mympd.example.org</command></para>
+            <para>
+                <emphasis>To black list a specific artist</emphasis> (not
+                currently playing) you can use <option>--bl_ar="Artist name to black list"</option>.
+            </para>
+        </refsect2>
+    </refsect1>
+    <refsect1 id="options">
+        <title>OPTIONS</title>
+        <para>The program follows the usual GNU command line syntax,
+            with long options starting with two dashes ("-").  A summary of
+            options is included below.</para>
+        <variablelist>
+            <!-- Use the variablelist.term.separator and the
+            variablelist.term.break.after parameters to
+            control the term elements. -->
+            <varlistentry> <!-- help -->
+                <term><option>-h</option></term>
+                <term><option>--help</option></term>
+                <listitem>
+                    <para>Print help and exit.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- version -->
+                <term><option>--version</option></term>
+                <listitem>
+                    <para>Print version and exit.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- add similarity -->
+                <term><option>-a <replaceable class="parameter">similarity_string</replaceable></option></term>
+                <term><option>--add_similarity=<replaceable class="parameter">similarity_string</replaceable></option></term>
+                <listitem>
+                    <para>Add similarity to the database.<sbr />For more details about the <replaceable class="option">similarity_string</replaceable> see <xref linkend="simiformat"/>.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- check_names -->
+                <term><option>-c</option></term>
+                <term><option>--check_names</option></term>
+                <listitem>
+                    <para>Use with <option>--add_similarity</option> in order to check artists names used in <replaceable class="parameter">similarity_string</replaceable>. &dhutils; will control presence of artists names in MPD library. Default is to look for MPD server on localhost:6600 or environment variables <envar>MPD_HOST</envar> and <envar>MPD_PORT</envar> if set.<sbr />You can as well give &dhutils; host/port on the command line using respectively <option>-S</option> and <option>-P</option>.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- black list artist -->
+                <term><option>--bl_art=<replaceable class="parameter">artist_name</replaceable></option></term>
+                <listitem>
+                    <para>Use to black list <replaceable class="parameter">artist_name</replaceable>. &dhutils; is checking <replaceable class="parameter">artist_name</replaceable> is actually in MPD music library (cf <option>-S</option> and <option>-P</option> options to set MPD host/address if necessary).</para>
+                    <para>If <replaceable class="parameter">artist_name</replaceable> is not found the script print out a list of matching artists.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- black list -->
+                <term><option>--bl_curr_trk</option> | <option>--bl_curr_art</option> | <option>--bl_curr_alb</option></term>
+                <listitem>
+                    <para>Use to black list the currently playing track|artist|album. You need access to your MPD server, use <option>-S</option> and <option>-P</option> to set MPD host/address if necessary.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- dbfile -->
+                <term><option>-d <replaceable class="parameter">db_file</replaceable></option></term>
+                <term><option>--dbfile=<replaceable class="parameter">db_file</replaceable></option></term>
+                <listitem>
+                    <para>Use the specific file <replaceable>db_file</replaceable> as database.<sbr />Default is too use <envar>XDG_DATA_HOME</envar> (see <xref linkend="files"/>).</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- purge history -->
+                <term><option>--purge_hist</option></term>
+                <listitem>
+                    <para>Purge history, you may supply an alternative DB file with --dbfile option.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- reciprocal -->
+                <term><option>-r</option></term>
+                <term><option>--reciprocal</option></term>
+                <listitem>
+                    <para>Use with an editing options in order to edit reciprocal similarity as well. <option>--add_similarity</option> and <option>--remove_{artist|similarity}</option> are supporting reciprocal edition.</para>
+                    <para><emphasis>N.B</emphasis>: this option has to appear after the editing option on the command line.</para>
+                    <para>See <xref linkend="simadb"/> for further information about reciprocity notion.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- remove artist -->
+                <term><option>--remove_artist=<replaceable class="parameter">artist</replaceable></option></term>
+                <listitem>
+                    <para>Use to remove an artist entry (as main artist) with its associated similarities. To remove artist where it appears as a similar artist use the <option>--reciprocal</option> option.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- remove bl id -->
+                <term><option>--remove_bl=<replaceable class="parameter">row_id</replaceable></option></term>
+                <listitem>
+                    <para>Use to remove a black list entry. To get the row_id to suppress use <option>--view_bl</option> option.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- remove similarity -->
+                <term><option>--remove_similarity=<replaceable class="parameter">"main artist,similar artist"</replaceable></option></term>
+                <listitem>
+                    <para>Use to remove a single similarity between a main artist and an associated similarity. Give the main artist first, use comma (",") to separate it from similar artist.<sbr />Use of <option>--reciprocal</option> is possible here, see <xref linkend="simadb" />.</para>
+                    <para>This option is useful in case you want to remove only a specific similarity between two artists, to remove completely an artist use <option>--remove_artist</option> instead.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- view artist -->
+                <term><option>-v <replaceable class="parameter">"artist name"</replaceable></option></term>
+                <term><option>--view_artist=<replaceable class="parameter">"artist name"</replaceable></option></term>
+                <listitem>
+                    <para>Get entries for <replaceable class="parameter">"artist name"</replaceable> in the data base (print to stdout).</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- view bl -->
+                <term><option>--view_bl</option></term>
+                <listitem>
+                    <para>Get all entries in the black list.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry> <!-- view all -->
+                <term><option>--view_all</option></term>
+                <listitem>
+                    <para>Get all entries in the data base (print to stdout).</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry>
+                <term><option>-P <replaceable class="parameter">mpd_port</replaceable></option></term>
+                <term><option>--port=<replaceable class="parameter">mpd_port</replaceable></option></term>
+                <listitem>
+                    <para>Use the specific port number <replaceable>mpd_port</replaceable> on MPD server. This overrides <envar>MPD_PORT</envar> environment variable.<sbr />Default is <emphasis>6600</emphasis>.</para>
+                </listitem>
+            </varlistentry>
+            <varlistentry>
+                <term><option>-S <replaceable class="parameter">mpd_host</replaceable></option></term>
+                <term><option>--host=<replaceable class="parameter">mpd_host</replaceable></option></term>
+                <listitem>
+                    <para>Use the specific host <replaceable>mpd_host</replaceable> as MPD server.<sbr /><replaceable>mpd_host</replaceable> can be an <acronym>IP</acronym> or a fully qualified domain name as long as your system can resolve it. This overrides <envar>MPD_HOST</envar> environment variable.<sbr />Default is <emphasis>localhost</emphasis>.</para>
+                </listitem>
+            </varlistentry>
+        </variablelist>
+    </refsect1>
+    <refsect1 id="files">
+        <title>FILES</title>
+        <variablelist>
+            <varlistentry>
+                <term><filename>${XDG_DATA_HOME}/mpd_sima/sima.db</filename></term>
+                <listitem>
+                    <para>SQLite DB file. Usually <envar>XDG_DATA_HOME</envar> is set to <filename>${HOME}/.local/share</filename>.</para>
+                </listitem>
+            </varlistentry>
+        </variablelist>
+    </refsect1>
+    <refsect1 id="simiformat">
+        <title>SIMILARITY FORMAT</title>
+        <para>The <replaceable class="parameter">similarity_string</replaceable> has to be formatted following a special pattern in order for simadb_cli to extract similarity relations between artists names. Usually a similarity entry is defined as a main artist, lets say <emphasis>main_art</emphasis>, followed by a list of similar artists which you want to be related to that <emphasis>main_art</emphasis>, each artist of that list with a specific similarity value, a match score, quantifying the similarity relation with the <emphasis>main_art</emphasis>. The match score value is an integer in [0 ,100] with 100 corresponding to a perfect match.</para>
+        <para><replaceable class="parameter">similarity_string</replaceable> is then to be formatted as follow:</para>
+        <para><command>main_art,first artist:<score>,second artist:<score></command></para>
+        <para>Each artist group are separated with commas (",") and inside each group the artist name and the match score is colon (":") separated. Obviously the first artist group, as the main artist, does not have a match score.</para>
+        <para>Lets see how it works with an example. I consider "Led Zeppelin" to be similar to "Tool" with a match score of 25, I also want to have "Audioslave" related to "Led Zeppelin" with a score of 20. Then the <replaceable class="parameter">similarity_string</replaceable> will be the following:</para>
+        <para><command>Led Zeppelin,Tool:25,Audiosalve:20</command></para>
+        <para>See <xref linkend="simadb" /> for more details about how similarities are handled</para>
+    </refsect1>
+    <refsect1 id="simadb">
+        <title>A WORD ABOUT SIMA DATA BASE</title>
+        <para>The similarity database is defined from the point of view of a <emphasis>main artist</emphasis> which is declared related to a list of <emphasis>similar artists</emphasis>. That means when you define <emphasis>main_art</emphasis> to be similar to <emphasis>sim_art A</emphasis> and <emphasis>sim_art B</emphasis> the reciprocal won't be true, <emphasis>sim_art A</emphasis> and <emphasis>sim_art B</emphasis> are not similar to <emphasis>main_art</emphasis>. At least this is the default behavior when you edit entries with simadb_cli, this is also the way last.fm is working concerning similar artists. This documentation is using that particular terminology to specify which kind of artist we are dealing with : "main artist" or "similar artist".<sbr />The <option>--reciprocal</option> option allows one to add reciprocal relation where <emphasis>sim_art A</emphasis> and <emphasis>sim_art B</emphasis> become respectively the <emphasis>main_art</emphasis>. Using <option>--reciprocal</option> you will then edit two more entries in the database. To summarize here is what you'll end up with in your data base adding similarity with this string <command>main_art,sim_art A:34,sim_art B:45</command>.</para>
+        <para><command>simadb_cli --reciprocal --add_similarity=main_art,sim_art A:34,sim_art B:45</command></para>
+        <para>main_art similar to sim_art A:34 and sim_art B:45<sbr />sim_art A  similar to main_art:34<sbr />sim_art B  similar to main_art:45</para>
+        <para>Without the reciprocal option you would have add only the first similarity. Usually using the reciprocal option is the desired behavior, at least what users have in mind when thinking of similarity relation between to artists but keep in mind that it may lead to have MPD_sima more sensible to loop over the same two artist (ASSERTION TO BE CONFIRMED).</para>
+    </refsect1>
+    <!--
+    <refsect1 id="diagnostics">
+        <title>DIAGNOSTICS</title>
+        <para>The following diagnostics may be issued
+            on <filename class="devicefile">stderr</filename>:</para>
+        <variablelist>
+            <varlistentry>
+                <term><errortext>Bad configuration file. Exiting.</errortext></term>
+                <listitem>
+                    <para>The configuration file seems to contain a broken configuration
+                        line. Use the <option>-''-verbose</option> option, to get more info.
+                    </para>
+                </listitem>
+            </varlistentry>
+        </variablelist>
+        <para><command>&dhpackage;</command> provides some return codes, that can
+            be used in scripts:</para>
+        <segmentedlist>
+            <segtitle>Code</segtitle>
+            <segtitle>Diagnostic</segtitle>
+            <seglistitem>
+                <seg><errorcode>0</errorcode></seg>
+                <seg>Program exited successfully.</seg>
+            </seglistitem>
+            <seglistitem>
+                <seg><errorcode>1</errorcode></seg>
+                <seg>The configuration file seems to be broken.</seg>
+            </seglistitem>
+        </segmentedlist>
+    </refsect1>
+    -->
+  <xi:include href="feedback.xml" />
+  <xi:include href="seealso.xml" />
+</refentry>
diff --git a/doc/Changelog b/doc/Changelog
new file mode 100644
index 0000000..0506bd7
--- /dev/null
+++ b/doc/Changelog
@@ -0,0 +1,249 @@
+sima v0.12.0
+
+ * Major refactoring
+ * Add a setup.py, got rid of the Makefile.
+ * Switched to Python3 (>=3.2)
+ * Depends on requests (http client)
+ * Depends on python-musicpd (py3k port of python-mpd)
+   configuration file change
+   internal database remains the same
+ * Temporarily removed userdb feature
+
+-- kaliko jack <kaliko at azylum.org> UNRELEASED
+
+
+sima v0.11.0
+
+ * straight forward py3k conversion from v0.10.0
+
+-- kaliko jack <kaliko at azylum.org> UNRELEASED
+
+
+sima v0.10.0
+
+ * Improved album detection (especially multi-artists album)
+ * Controls conf file is readable at startup
+ * Complete rewrite of MPD client
+
+-- kaliko jack <kaliko at azylum.org>  Wed, 26 Sep 2012 18:56:57 +0200
+
+
+sima v0.9.2
+
+ * Fixed Makefile, thanks Artur Frysiak (fixes #2849 #2848)
+
+-- kaliko jack <kaliko at azylum.org>  Sun, 26 Feb 2012 22:06:27 +0100
+
+
+sima v0.9.1
+
+ * Fixed an issue in idle implementation
+
+-- kaliko jack <kaliko at azylum.org>  Fri, 24 Feb 2012 12:04:39 +0100
+
+
+sima v0.9.0
+
+ * New CLI option --create-db
+ * Option main_loop_time removed, use of idle makes it useless
+ * New config file option "dynamic" (fixes #2593)
+
+ Refactoring:
+ * Start player abstraction, new MPD class (fixes #2418)
+ * Use idle MPD command, needs python-mpd >= 0.3
+
+ Bugs fix:
+ * PID file creation now respects system umask (fixes #2368)
+ * Avoid to queue twice the same album (fixes #2595)
+ * Empty password does not trigger auth attempt (fixes #2543)
+ * Fixed --var_dir option, thanks Artur Frysiak (fixes #2796)
+
+ Minor changes:
+ * Improved execution time in tracks history look up mainly.
+ * Switch to python-daemon 0.4
+
+-- kaliko jack <kaliko at azylum.org>  Fri, 10 Feb 2012 11:31:57 +0100
+
+
+sima v0.8.0
+
+ * New CLI option "--daemon" to run as a daemon
+
+ * Abandoned compatibility with python 2.5 (urllib2.urlopen timeout)
+ * Add daemon cli option to daemonize the process (closes #616)
+ * Add a Makefile (install, uninstall targets) following GNU standards.
+ * Refactored queue_mode method, thanks MsieurHappy ;)
+ * Moved Track class to separate file.
+ * Reduce memory footprint (better simafm cache handling).
+
+ * Fixed a bug in database management, cleaning database performs better
+
+-- kaliko jack <kaliko at azylum.org>  Sun, 08 May 2011 14:06:22 +0200
+
+
+sima v0.7.2
+
+  * Fixes a bug introduced fixing #2113, history was no longer honored!
+
+-- kaliko jack <kaliko at azylum.org>  Sun, 30 Jan 2011 10:54:53 +0100
+
+
+sima v0.7.1
+
+ * Add Makefile from 0.8 dev branch
+ * Fixes #2113
+ * Fixes #2091
+
+-- kaliko jack <kaliko at azylum.org>  Sat, 22 Jan 2011 09:17:18 +0100
+
+
+sima v0.7.0
+
+ * Stall queueing when MPD is in "single" or "repeat" mode (closes #1607)
+ * No longer need to restart MPD_sima when MPD database has been updated, it
+   was necessary to have new entries to appear in MPD_sima (closes #1719).
+ * New database version, upgraded at first start (back up done in sima.db.0.6)
+ * Uses SQLite for history. Play history always saved, not anymore optional.
+ * simadb_cli uses MPD_HOST/MPD_PORT environment variables
+ 
+ * New mpd_sima CLI option --var_dir to change default ${XDG_DATA_HOME}
+   Allows to launch the script as a "system service" (closes #1738)
+ 
+ Bugs:
+ * Autoqueue not anymore stall when started without internet access (closes
+   #1568 and #1695)
+ * Fixed a bug with MPD_(HOST|PORT), env. var. only used when no host/port are
+   set in the config file.
+ 
+ Configuration file changes:
+ * New "history_duration" option
+ * Remove "history" & "history_length" options, replace by "history_duration"
+ * New "queue" option
+ * Remove "top_tracks" option, now use "queue" option with "top" value.
+ * New consume option (closes #1576)
+ * New queue mode "album" (closes #1008)
+ * New option "album_to_queue" to set how many albums to queue
+ * New option "single_album" allowing to force queuing track from different
+   albums, for instance it'll avoid to end up playing a whole OST.
+
+ (cf. <doc/examples/all_settings.cfg> for all options available)
+
+-- kaliko jack <kaliko at azylum.org>  Sat, 18 Dec 2010 12:11:12 +0100
+
+
+sima v0.6.0
+
+ * Use of SQLite (closes #838)
+ * [simadb_cli] new command line interface to edit sima.db (SQLite database)
+ * [mpd_sima.py] New command line option to load a specific configuration file
+ * Removed pyscrobbler module, now uses SimaFM (closes #741)
+ * Cache is no longer managed as it was (because of SimaFM), it isn't
+   possible to save it (advanced "cache" option removed).
+   last.fm request caching is still enabled though.
+   Saving last.fm request to file is milestoned 0.6.1
+ * Create documentation (docbook -> {troff,pdf,(x)html})
+ * Convert to unicode all utf-8/legacy code (closes #839)
+ * Re-Licenced the code under GNU GPL.
+
+-- kaliko jack <kaliko at azylum.org>  Mon, 10 May 2010 19:40:10 +0200
+
+
+sima v0.5.2
+
+ * New fuzzy string matching dedicated to artist names (closes #686)
+ * uses optparse instead of getopt (closes #834)
+ * now sets options with built-in default, then configuration file and finally
+   command line.
+ * Fixes the “deprecated 'md5'” warning in audioscrobbler.py with patch
+   provided in bug report #11 (closes #1144) –  cf. pyscrobbler online:
+   http://code.google.com/p/pyscrobbler
+
+-- kaliko jack <kaliko at azylum.org>  Tue, 16 Mar 2010 14:01:22 +0100
+
+
+sima v0.5.1
+
+ * No longer uses/writes a default conf file in ~/.config/mpd_sima (or XDG
+   default), MPD_HOST is used if no conf file is present.
+ * replace exhaustive loops by generator
+ * clean up
+
+-- kaliko jack <kaliko at azylum.org>  Thu, 26 Nov 2009 10:13:08 +0100
+
+
+sima v0.5.0
+
+ * Add option to play only most popular songs from an artist (closes #739).
+ * Now prefer non played over already played artist is possible (closes #774)
+ * Use of %-age of similarity to get the list of similar (closes #740).
+ * nearly all Unicode (closes #742).
+ * Using dequeu object => ** New history/cache/user_db formats. **
+ * Heavy refactoring, new Track object.
+ * Improved fuzzy search for artists in MPD library (using difflib/levenshtein)
+ * Log to file within python script (closes #815), add --log=<file> option.
+ * Removed bashism & improved portability in shell launcher, thanks ksh at vim-fr ;)
+
+-- kaliko jack <kaliko at azylum.org>  Sun, 25 Oct 2009 12:02:02 +0100
+
+
+sima v0.4.0
+
+ * Add option to play only most popular songs from an artist
+ * Better list of similar artists thanks to %-age of similarity… hopefully
+ * Unicode !!!!
+
+-- kaliko jack Unreleased / abandoned dev branch
+
+
+sima v0.3.0
+
+ * New bash wrapper launch.sh to execute the script
+ * User's database for similar artists through a new conf file user_db_cfg.
+   Refer to examples for syntax.
+ * check playlist queue to add tracks, not only history.
+ * Rename history to history.pkl
+ * add new command line options, help and version.
+ * now look for MPD_HOST MPD_PORT env variable to create default conf file
+ * run main loop even if the same track is playing, hence MPD_sima will keep
+   adding new tracks even if the same track is still playing and stops only if
+   the target is reached (ie. queue_length)
+
+-- kaliko jack <kaliko at azylum.org>  Tue, 28 Jul 2009 18:10:09 +0200
+
+
+sima v0.2.0
+
+ * WARNING: Moved to python-mpd instead of python-mpdclient2
+ * Add cache for last.fm request
+ * Possibility to save cache to file (new option in mpd_sima.cfg)
+ * new src directory layout
+ * Improved log output formatting
+ * Imroved code & removed old unused method
+ * Controls availability of commands right after conn/auth
+ * New pyscroobler version (revision 5), fix bug with slash in names.
+
+-- kaliko jack <kaliko at azylum.org>  Fri, 12 Jun 2009 18:10:47 +0200
+
+
+sima v0.1.1 (first upgrade)
+
+  * HEAVY refactoring to improve coding style (thanks pylint).
+  * intercept audioScrobbler server connexion errors (no longer stops
+    mpd_sima). Issue/bug reported:
+    http://code.google.com/p/pyscrobbler/issues/detail?id=7
+  * now cath SIGTERM signal along with SIGINT (keyborad interupt)
+  * save history to file (new option in mpd_sima.cfg)
+  * add --pid option to python script
+  * correct launch.sh accordingly
+  * new date format for the log
+
+-- kaliko jack <kaliko at azylum.org>  Thu, 04 Jun 2009 20:35:25 +0200
+
+
+sima v0.1.0 (first stable release)
+
+  * First quasi stable release (my first python code…)
+
+-- kaliko jack <kaliko at azylum.org>  Thu, 28 May 2009 20:45:03 +0200
+
+# vim: fileencoding=utf-8
diff --git a/doc/examples/all_settings.cfg b/doc/examples/all_settings.cfg
index 7eb0f24..3be857d 100644
--- a/doc/examples/all_settings.cfg
+++ b/doc/examples/all_settings.cfg
@@ -4,16 +4,6 @@
 # your $XDG_CONFIG_HOME (default is $HOME/.config/sima/)
 # You can also call it with --config option.
 #
-# Pay Attention:
-# * Inline comment are not possible
-#
-#  WRONG:
-#  host = localhost  # My host
-#
-#  OK:
-#  # My host
-#  host = localhost
-#
 ########################################################################
 
 ########################## MPD SECTION ################################
@@ -48,7 +38,6 @@ port = 6600
 # description: file to log to. Usually used in daemon mode.
 # default: unset, logging to stdin/stdout
 #logfile =
-##
 
 ## VERBOSITY
 # type: string
@@ -60,7 +49,6 @@ port = 6600
 #    * warning
 #    * error
 verbosity = info
-##
 
 #
 #######################################################################
@@ -76,16 +64,20 @@ verbosity = info
 #                contrib = Scrobble, AwesomePlugin,
 #                          ExperimentalTest, AnotherTest
 # default:
+#          internal = "Crop, Lastfm, RandomFallBack"
+#          contrib =
 # description: Plugins list declaration.
-#     Optional plugin's configuration must be in its own section.
+#     Optional plugin's configuration lays in its own section.
 #     For instance a "AwesomePlugin" declared here
-#     gets its configuration from an "[AwesomePlugin]"
-#     or "[awesomeplugin]" section (case insensitive).
+#     gets its configuration from the corresponding section:
+#     "[awesomeplugin]"
+#     internal plugins will look for a section named after the lower-cased name
+#     of the pluglin, ie. RandomFallBack → randomfallback.
 #
 #     Two plugins sources are available, internal and contrib
 #
 internal = Crop, Lastfm, RandomFallBack
-#contrib = PlaceHolder
+#contrib =
 
 ## HISTORY_DURATION
 # type: integer (in hours)
@@ -94,34 +86,24 @@ internal = Crop, Lastfm, RandomFallBack
 #     track/title
 #
 history_duration = 8
-##
 
 ## USER_DB # NOT IMPLEMENTED #
 # type: boolean
 # description: Load user database to find similar artists
-#     User DB is loaded from $XDG_CONFIG_HOME/sima/sima.db
+#     User DB is loaded from $XDG_CONFIG_HOME/mpd_sima/sima.db
 #     Use simadb_cli to edit/add entries.
 user_db = false
-##
-
-#####################################################################
-# You do not need to set up options below.
-# But well, you got bored of the way sima is behaving, then go ahead
-# play with it :)
 
 ## QUEUE_LENGTH
 # type: integer
 # default: 1
 # description: Queue length triggering tracks addition
 queue_length = 1
-##
 
 ######################### PLUGINS #####################################
-#
-[placeholder]
-key = Value
 
 [crop]
+## CONSUME
 # type: integer
 # default: unset, not cropping playlist
 # description: How many played tracks to keep in the playlist.
@@ -129,7 +111,8 @@ key = Value
 #  Leave commented to keep all tracks
 #consume = 10
 
-[RandomFallback]
+[randomfallback]
+## FLAVOUR
 # type: string
 # default: sensible
 # description: When no similar tracks are found, falling back to random
@@ -138,34 +121,37 @@ key = Value
 #       * pure:     complete random choice among all tracks available in the
 #                   player media library
 #       * sensible: use play history to filter chosen tracks
-#       * genre:    chose among the same genre as current track (using genre
+#       * genre:    # NOT IMPLEMENTED #
+#                   chose among the same genre as current track (using genre
 #                   tag). If no genre tag is available "sensible" flavour
 #                   is used  instead
 #flavour=sensible
 
+## TRACK_TO_ADD
+# type: integer
+# description: how many tracks the plugin will try to get
+# default: 1
+#track_to_add = 1
+
+
+# EchoNest or LastFM
+#[echonest]
 [lastfm]
-## QUEUE_MODE # NOT COMPLETED #
+## QUEUE_MODE
 # type: string
 # description: The default is to queue random tracks from similar artists.
 # Possible values:
 #	track : Will queue tracks from similar artists (default).
-#	top   : Will queue top tracks from similar artists. # NOT IMPLEMENTED #
+#	top   : Will queue top tracks from similar artists.
 #	album : Will queue whole album from similar artists.
 queue_mode = track
 
-## SIMILARITY
-# type: integer in [0 100]
-# description: Similarity as a percentage of similarity between artists
-# (this is a last.fm metric)
-similarity = 15
-
-## DYNAMIC
+## MAX_ART
 # type: integer
 # description: Number of similar artist to retrieve from local media library.
 #  When set to something superior to zero, MPD_sima tries to get as much similar
-#  artists from media library provided artists similarity is superior to
-#  similarity value.
-dynamic = 10
+#  artists from media library
+max_art = 10
 
 ## DEPTH
 # type: integer in [1, +∞]
@@ -176,7 +162,8 @@ depth = 1
 ## SINGLE_ALBUM
 # type: boolean
 # scope: "track" and "top" queue modes
-# description: Prevent from queueing a track from the same album (for instance with OST).
+# description: Prevent from queueing a track from the same album (for instance
+#  with OST).
 single_album = false
 
 ## TRACK_TO_ADD
@@ -191,10 +178,18 @@ track_to_add = 1
 # description: how many albums the plugin will try to get
 album_to_add = 1
 
-#
-#######################################################################
+## CACHE
+# type: boolean
+# description: whether or not to use on-disk persistent http cache
+#  * When set to "true", sima will use a persistent cache for its http client.
+#    The cache is written along with the dbfile in:
+#                $XDG_CONFIG_HOME/mpd_sima/http/<web_service>
+#    Toggling http cache is only available for last.fm. EchoNest have rate limits,
+#    we must then pay attention to bandwidth and use of caching is required.
+#  * If set to "false", caching is still done but in memory.
+# default: True
+cache = True
 
 #
 ####################### END OF CONFIGURATION ##########################
-
 # vim: syntax=cfg fileencoding=utf-8
diff --git a/mpd-sima b/mpd-sima
old mode 100644
new mode 100755
diff --git a/setup.py b/setup.py
index b61594a..6860dd9 100755
--- a/setup.py
+++ b/setup.py
@@ -11,11 +11,10 @@ from sima.info import __version__ as VERSION, __author__ as AUTHOR
 from sima.info import __doc__ as DESCRIPTION, __email__ as EMAIL
 
 data_files = [
-    ('share/man/man1', ['data/mpd-sima.1',]),
-    #('share/man/man1', ['data/mpd-sima.1', 'data/simadb_cli.1',]),
-    #('share/man/man5', ['data/mpd-sima.cfg.5',]),
+    ('share/man/man1', ['data/man/mpd-sima.1', 'data/man/simadb_cli.1',]),
+    ('share/man/man5', ['data/man/mpd_sima.cfg.5',]),
     ('share/doc/mpd-sima/examples/', glob.glob('doc/examples/*')),
-    #('share/doc/mpd-sima/', [fi for fi in listdir('doc') if isfile(fi)]),
+    ('share/doc/mpd-sima/', [fi for fi in listdir('doc') if isfile(fi)]),
 ]
 classifiers = [
         "Development Status :: 5 - Production/Stable",
@@ -44,7 +43,7 @@ setup(name='sima',
       packages=find_packages(),
       include_package_data=True,
       data_files=data_files,
-      #scripts=['mpd-sima'],
+      scripts=['simadb_cli'],
       entry_points={
           'console_scripts': ['mpd-sima = sima.launch:main',]
           },
diff --git a/sima/__init__.py b/sima/__init__.py
index d9d4fd5..267899c 100644
--- a/sima/__init__.py
+++ b/sima/__init__.py
@@ -1,4 +1,7 @@
 # -*- coding: utf-8 -*-
+"""WebServices API credentials and ressources
+"""
+from datetime import timedelta
 
 LFM = {'apikey': 'NG4xcDlxcXJwMjk4MTZycTgwM3E3b3I5MTEzb240cG8',
        'host':'ws.audioscrobbler.com',
@@ -10,5 +13,7 @@ ECH = {'apikey': 'WlRKQkhTS0JHWFVDUEZZRFA',
        'version': 'v4',
        }
 
+WAIT_BETWEEN_REQUESTS = timedelta(0, 2)
+SOCKET_TIMEOUT = 6
 
 # vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/sima/client.py b/sima/client.py
index 6cd3802..c04b259 100644
--- a/sima/client.py
+++ b/sima/client.py
@@ -1,4 +1,22 @@
-# -* coding: utf-8 -*-
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
 """MPD client for Sima
 
 This client is built above python-musicpd a fork of python-mpd
@@ -23,6 +41,7 @@ from .lib.player import Player
 from .lib.track import Track
 from .lib.meta import Album
 from .lib.simastr import SimaStr
+from .utils.leven import levenshtein_ratio
 
 
 class PlayerError(Exception):
@@ -47,11 +66,17 @@ def blacklist(artist=False, album=False, track=False):
             #bl_getter = next(fn for fn, bl in zip(bl_fun, boolgen) if bl is True)
             bl_getter = next(dropwhile(lambda _: not next(boolgen), bl_fun))
             #cls.log.debug('using {0} as bl filter'.format(bl_getter.__name__))
-            results = func(*args, **kwargs)
-            for elem in results:
+            results = list()
+            for elem in func(*args, **kwargs):
                 if bl_getter(elem, add_not=True):
-                    cls.log.info('Blacklisted: {0}'.format(elem))
-                    results.remove(elem)
+                    cls.log.debug('Blacklisted "{0}"'.format(elem))
+                    continue
+                if track and cls.database.get_bl_album(elem, add_not=True):
+                    # filter album as well in track mode
+                    # (artist have already been)
+                    cls.log.debug('Blacklisted alb. "{0.album}"'.format(elem))
+                    continue
+                results.append(elem)
             return results
         return wrapper
     return decorated
@@ -130,7 +155,7 @@ class PlayerClient(Player):
             or not hasattr(old_curr, 'id')
             or not hasattr(self.current, 'id')):
             return False
-        return (self.current.id != old_curr.id)  # pylint: disable=no-member
+        return self.current.id != old_curr.id  # pylint: disable=no-member
 
     def _flush_cache(self):
         """
@@ -145,12 +170,36 @@ class PlayerClient(Player):
                 }
         self._cache['artists'] = frozenset(self._client.list('artist'))
 
+    @blacklist(track=True)
     def find_track(self, artist, title=None):
         #return getattr(self, 'find')('artist', artist, 'title', title)
         if title:
             return self.find('artist', artist, 'title', title)
         return self.find('artist', artist)
 
+    @blacklist(track=True)
+    def fuzzy_find_track(self, artist, title):
+        # Retrieve all tracks from artist
+        all_tracks = self.find('artist', artist)
+        # Get all titles (filter missing titles set to 'None')
+        all_artist_titles = frozenset([tr.title for tr in all_tracks
+                                       if tr.title is not None])
+        match = get_close_matches(title, all_artist_titles, 50, 0.78)
+        if not match:
+            return []
+        for title_ in match:
+            leven = levenshtein_ratio(title.lower(), title_.lower())
+            if leven == 1:
+                pass
+            elif leven >= 0.79:  # PARAM
+                self.log.debug('title: "%s" should match "%s" (lr=%1.3f)' %
+                               (title_, title, leven))
+            else:
+                self.log.debug('title: "%s" does not match "%s" (lr=%1.3f)' %
+                               (title_, title, leven))
+                return []
+            return self.find('artist', artist, 'title', title_)
+
     @blacklist(artist=True)
     def fuzzy_find_artist(self, art):
         """
@@ -223,11 +272,15 @@ class PlayerClient(Player):
             if album not in albums:
                 albums.append(Album(name=album, **kwalbart))
         for album in self.list('album', 'artist', artist):
-            arts = set([trk.artist for trk in self.find('album', album)])
-            if len(arts) < 2:  # TODO: better heuristic, use a ratio instead
+            album_trks = [trk for trk in self.find('album', album)]
+            if 'Various Artists' in [tr.albumartist for tr in album_trks]:
+                self.log.debug('Discarding {0} ("Various Artists" set)'.format(album))
+                continue
+            arts = set([trk.artist for trk in album_trks])
+            if len(set(arts)) < 2:  # TODO: better heuristic, use a ratio instead
                 if album not in albums:
                     albums.append(Album(name=album, albumartist=artist))
-            elif (album and album not in albums):
+            elif album and album not in albums:
                 self.log.debug('"{0}" probably not an album of "{1}"'.format(
                                album, artist) + '({0})'.format('/'.join(arts)))
         return albums
@@ -270,7 +323,7 @@ class PlayerClient(Player):
     def queue(self):
         plst = self.playlist
         plst.reverse()
-        return [ trk for trk in plst if int(trk.pos) > int(self.current.pos)]
+        return [trk for trk in plst if int(trk.pos) > int(self.current.pos)]
 
     @property
     def playlist(self):
diff --git a/sima/core.py b/sima/core.py
index 8e075b0..634db4b 100644
--- a/sima/core.py
+++ b/sima/core.py
@@ -1,8 +1,25 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2009, 2010, 2011, 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
 """Core Object dealing with plugins and player client
 """
 
-import sys
 import time
 
 from collections import deque
@@ -31,9 +48,7 @@ class Sima(Daemon):
         try:
             self.player.connect()
         except (PlayerError, PlayerUnHandledError) as err:
-            self.log.error('Fails to connect player: {}'.format(err))
-            self.shutdown()
-            sys.exit(1)
+            self.log.warning('Player: {}'.format(err))
         self.short_history = deque(maxlen=60)
 
     def __get_player(self):
@@ -44,6 +59,7 @@ class Sima(Daemon):
         return PlayerClient(host, port, pswd)
 
     def add_history(self):
+        """Handle local short history"""
         self.short_history.appendleft(self.player.current)
 
     def register_plugin(self, plugin_class):
@@ -57,6 +73,7 @@ class Sima(Daemon):
             getattr(plugin, method)(*args, **kwds)
 
     def need_tracks(self):
+        """Is the player in need for tracks"""
         if not self.enabled:
             self.log.debug('Queueing disabled!')
             return False
@@ -71,13 +88,13 @@ class Sima(Daemon):
     def queue(self):
         to_add = list()
         for plugin in self.plugins:
-            pl_callback =  getattr(plugin, 'callback_need_track')()
+            pl_callback = getattr(plugin, 'callback_need_track')()
             if pl_callback:
                 to_add.extend(pl_callback)
         if not to_add:
             self.log.warning('Queue plugins returned nothing!')
             for plugin in self.plugins:
-                pl_callback =  getattr(plugin, 'callback_need_track_fb')()
+                pl_callback = getattr(plugin, 'callback_need_track_fb')()
                 if pl_callback:
                     to_add.extend(pl_callback)
         for track in to_add:
@@ -129,13 +146,13 @@ class Sima(Daemon):
             except PlayerUnHandledError as err:
                 #TODO: unhandled Player exceptions
                 self.log.warning('Unhandled player exception: {}'.format(err))
-                del(self.player)
+                del self.player
                 self.player = PlayerClient()
                 time.sleep(10)
             except PlayerError as err:
                 self.log.warning('Player error: %s' % err)
                 self.reconnect_player()
-                del(self.changed)
+                del self.changed
 
     def loop(self):
         """Dispatching callbacks to plugins
diff --git a/sima/info.py b/sima/info.py
index 3b6d369..5bf5084 100644
--- a/sima/info.py
+++ b/sima/info.py
@@ -11,7 +11,7 @@ queue is getting short.
 """
 
 
-__version__ = '0.12.0pr2'
+__version__ = '0.12.0pr4'
 __author__ = 'kaliko jack'
 __email__ = 'kaliko at azylum.org'
 __url__ = 'git://git.kaliko.me/sima.git'
diff --git a/sima/launch.py b/sima/launch.py
index 24802bf..19fac0c 100644
--- a/sima/launch.py
+++ b/sima/launch.py
@@ -1,4 +1,22 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
 """Sima
 """
 
@@ -31,7 +49,7 @@ def load_plugins(sima, source):
         sima:   sima.core.Sima instance
         source: ['internal', 'contrib']
     """
-    if not sima.config.get('sima', source ):
+    if not sima.config.get('sima', source):
         return
     logger = logging.getLogger('sima')
     for plugin in sima.config.get('sima', source).split(','):
@@ -40,7 +58,8 @@ def load_plugins(sima, source):
         try:
             mod_obj = __import__(module, fromlist=[plugin])
         except ImportError as err:
-            logger.error('Failed to load plugin\'s module: {0} ({1})'.format(module, err))
+            logger.error('Failed to load plugin\'s module: ' +
+                         '{0} ({1})'.format(module, err))
             sima.shutdown()
             sys.exit(1)
         try:
@@ -49,7 +68,8 @@ def load_plugins(sima, source):
             logger.error('Failed to load plugin {0} ({1})'.format(plugin, err))
             sima.shutdown()
             sys.exit(1)
-        logger.info('Loading {0} plugin: {name} ({doc})'.format(source, **plugin_obj.info()))
+        logger.info('Loading {0} plugin: {name} ({doc})'.format(
+                             source, **plugin_obj.info()))
         sima.register_plugin(plugin_obj)
 
 
@@ -115,13 +135,14 @@ def run(sopt, restart=False):
     # pylint: disable=broad-except
     try:
         start(sopt, restart)
-    except SigHup as err:  # SigHup inherit from Exception
+    except SigHup:  # SigHup inherit from Exception
         run(sopt, True)
     except Exception:  # Unhandled exception
         exception_log()
 
 # Script starts here
 def main():
+    """Entry point"""
     nfo = dict({'version': info.__version__,
                  'prog': 'sima'})
     # StartOpt gathers options from command line call (in StartOpt().options)
diff --git a/sima/lib/__init__.py b/sima/lib/__init__.py
index e69de29..96e495e 100644
--- a/sima/lib/__init__.py
+++ b/sima/lib/__init__.py
@@ -0,0 +1 @@
+# pylint: disable=missing-docstring
diff --git a/sima/lib/cache.py b/sima/lib/cache.py
new file mode 100644
index 0000000..e16ca0e
--- /dev/null
+++ b/sima/lib/cache.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2014 Jack Kaliko <kaliko at azylum.org>
+# Copyright (c) 2012, 2013 Eric Larson <eric at ionrock.org>
+#
+#   This program is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+"""
+The cache object API for implementing caches. The default is just a
+dictionary, which in turns means it is not threadsafe for writing.
+"""
+
+import os
+import codecs
+
+from hashlib import md5
+from pickle import load, dump
+from threading import Lock
+
+from ..utils.filelock import FileLock
+
+
+class BaseCache:
+
+    def get(self, key):
+        """Get cache value"""
+        raise NotImplementedError
+
+    def set(self, key, value):
+        """Set cache value"""
+        raise NotImplementedError
+
+    def delete(self, key):
+        """Remove cache value"""
+        raise NotImplementedError
+
+
+class DictCache(BaseCache):
+
+    def __init__(self, init_dict=None):
+        self.lock = Lock()
+        self.data = init_dict or {}
+
+    def get(self, key):
+        return self.data.get(key, None)
+
+    def set(self, key, value):
+        with self.lock:
+            self.data.update({key: value})
+
+    def delete(self, key):
+        with self.lock:
+            if key in self.data:
+                self.data.pop(key)
+
+
+class FileCache:
+
+    def __init__(self, directory, forever=False):
+        self.directory = directory
+        self.forever = forever
+
+        if not os.path.isdir(self.directory):
+            os.makedirs(self.directory)
+
+    def encode(self, val):
+        return md5(val.encode('utf-8')).hexdigest()
+
+    def _fn(self, name):
+        return os.path.join(self.directory, self.encode(name))
+
+    def get(self, key):
+        name = self._fn(key)
+        if os.path.exists(name):
+            return load(codecs.open(name, 'rb'))
+
+    def set(self, key, value):
+        name = self._fn(key)
+        with FileLock(name):
+            with codecs.open(name, 'w+b') as flh:
+                dump(value, flh)
+
+    def delete(self, key):
+        if not self.forever:
+            os.remove(self._fn(key))
+
+    def __iter__(self):
+        for dirpath, _, filenames in os.walk(self.directory):
+            for item in filenames:
+                name = os.path.join(dirpath, item)
+                yield load(codecs.open(name, 'rb'))
diff --git a/sima/lib/daemon.py b/sima/lib/daemon.py
index 06f7bee..fa09976 100644
--- a/sima/lib/daemon.py
+++ b/sima/lib/daemon.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-
 # Public Domain
 #
 # Copyright 2007, 2009 Sander Marechal <s.marechal at jejik.com>
@@ -62,8 +61,8 @@ class Daemon(object):
         for details (ISBN 0201563177)
 
         Short explanation:
-            Unix processes belong to "process group" which in turn lies within a "session".
-            A session can have a controlling tty.
+            Unix processes belong to "process group" which in turn lies within a
+            "session".  A session can have a controlling tty.
             Forking twice allows to detach the session from a possible tty.
             The process lives then within the init process.
         """
diff --git a/sima/lib/http.py b/sima/lib/http.py
new file mode 100644
index 0000000..c07ad38
--- /dev/null
+++ b/sima/lib/http.py
@@ -0,0 +1,267 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2014 Jack Kaliko <kaliko at azylum.org>
+# Copyright (c) 2012, 2013 Eric Larson <eric at ionrock.org>
+#
+#   This program is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+"""
+The httplib2 algorithms ported for use with requests.
+"""
+import re
+import calendar
+import time
+
+import email.utils
+
+from .cache import DictCache
+
+
+URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")
+
+
+def parse_uri(uri):
+    """Parses a URI using the regex given in Appendix B of RFC 3986.
+
+        (scheme, authority, path, query, fragment) = parse_uri(uri)
+    """
+    groups = URI.match(uri).groups()
+    return (groups[1], groups[3], groups[4], groups[6], groups[8])
+
+
+class CacheController(object):
+    """An interface to see if request should cached or not.
+    """
+    def __init__(self, cache=None, cache_etags=True):
+        self.cache = cache or DictCache()
+        self.cache_etags = cache_etags
+
+    def _urlnorm(self, uri):
+        """Normalize the URL to create a safe key for the cache"""
+        (scheme, authority, path, query, _) = parse_uri(uri)
+        if not scheme or not authority:
+            raise Exception("Only absolute URIs are allowed. uri = %s" % uri)
+        authority = authority.lower()
+        scheme = scheme.lower()
+        if not path:
+            path = "/"
+
+        # Could do syntax based normalization of the URI before
+        # computing the digest. See Section 6.2.2 of Std 66.
+        request_uri = query and "?".join([path, query]) or path
+        scheme = scheme.lower()
+        defrag_uri = scheme + "://" + authority + request_uri
+
+        return defrag_uri
+
+    def cache_url(self, uri):
+        return self._urlnorm(uri)
+
+    def parse_cache_control(self, headers):
+        """
+        Parse the cache control headers returning a dictionary with values
+        for the different directives.
+        """
+        retval = {}
+
+        # requests provides a CaseInsensitiveDict as headers
+        cc_header = 'cache-control'
+        if cc_header in headers:
+            parts = headers[cc_header].split(',')
+            parts_with_args = [
+                tuple([x.strip().lower() for x in part.split("=", 1)])
+                for part in parts if -1 != part.find("=")]
+            parts_wo_args = [(name.strip().lower(), 1)
+                             for name in parts if -1 == name.find("=")]
+            retval = dict(parts_with_args + parts_wo_args)
+        return retval
+
+    def cached_request(self, url, headers):
+        """Return the cached resquest if available and fresh
+        """
+        cache_url = self.cache_url(url)
+        cc = self.parse_cache_control(headers)
+
+        # non-caching states
+        no_cache = True if 'no-cache' in cc else False
+        if 'max-age' in cc and cc['max-age'] == 0:
+            no_cache = True
+
+        # see if it is in the cache anyways
+        in_cache = self.cache.get(cache_url)
+        if no_cache or not in_cache:
+            return False
+
+        # It is in the cache, so lets see if it is going to be
+        # fresh enough
+        resp = self.cache.get(cache_url)
+
+        # Check our Vary header to make sure our request headers match
+        # up. We don't delete it from the though, we just don't return
+        # our cached value.
+        #
+        # NOTE: Because httplib2 stores raw content, it denotes
+        #       headers that were sent in the original response by
+        #       adding -varied-$name. We don't have to do that b/c we
+        #       are storing the object which has a reference to the
+        #       original request. If that changes, then I'd propose
+        #       using the varied headers in the cache key to avoid the
+        #       situation all together.
+        if 'vary' in resp.headers:
+            varied_headers = resp.headers['vary'].replace(' ', '').split(',')
+            original_headers = resp.request.headers
+            for header in varied_headers:
+                # If our headers don't match for the headers listed in
+                # the vary header, then don't use the cached response
+                if headers.get(header, None) != original_headers.get(header):
+                    return False
+
+        now = time.time()
+        date = calendar.timegm(
+            email.utils.parsedate_tz(resp.headers['date'])
+        )
+        current_age = max(0, now - date)
+
+        # TODO: There is an assumption that the result will be a
+        # requests response object. This may not be best since we
+        # could probably avoid instantiating or constructing the
+        # response until we know we need it.
+        resp_cc = self.parse_cache_control(resp.headers)
+
+        # determine freshness
+        freshness_lifetime = 0
+        if 'max-age' in resp_cc and resp_cc['max-age'].isdigit():
+            freshness_lifetime = int(resp_cc['max-age'])
+        elif 'expires' in resp.headers:
+            expires = email.utils.parsedate_tz(resp.headers['expires'])
+            if expires is not None:
+                expire_time = calendar.timegm(expires) - date
+                freshness_lifetime = max(0, expire_time)
+
+        # determine if we are setting freshness limit in the req
+        if 'max-age' in cc:
+            try:
+                freshness_lifetime = int(cc['max-age'])
+            except ValueError:
+                freshness_lifetime = 0
+
+        if 'min-fresh' in cc:
+            try:
+                min_fresh = int(cc['min-fresh'])
+            except ValueError:
+                min_fresh = 0
+            # adjust our current age by our min fresh
+            current_age += min_fresh
+
+        # see how fresh we actually are
+        fresh = (freshness_lifetime > current_age)
+
+        if fresh:
+            # make sure we set the from_cache to true
+            resp.from_cache = True
+            return resp
+
+        # we're not fresh. If we don't have an Etag, clear it out
+        if 'etag' not in resp.headers:
+            self.cache.delete(cache_url)
+
+        if 'etag' in resp.headers:
+            headers['If-None-Match'] = resp.headers['ETag']
+
+        if 'last-modified' in resp.headers:
+            headers['If-Modified-Since'] = resp.headers['Last-Modified']
+
+        # return the original handler
+        return False
+
+    def add_headers(self, url):
+        resp = self.cache.get(url)
+        if resp and 'etag' in resp.headers:
+            return {'If-None-Match': resp.headers['etag']}
+        return {}
+
+    def cache_response(self, request, resp):
+        """
+        Algorithm for caching requests.
+
+        This assumes a requests Response object.
+        """
+        # From httplib2: Don't cache 206's since we aren't going to
+        # handle byte range requests
+        if resp.status_code not in [200, 203]:
+            return
+
+        cc_req = self.parse_cache_control(request.headers)
+        cc_resp = self.parse_cache_control(resp.headers)
+
+        cache_url = self.cache_url(request.url)
+
+        # Delete it from the cache if we happen to have it stored there
+        no_store = cc_resp.get('no-store') or cc_req.get('no-store')
+        if no_store and self.cache.get(cache_url):
+            self.cache.delete(cache_url)
+
+        # If we've been given an etag, then keep the response
+        if self.cache_etags and 'etag' in resp.headers:
+            self.cache.set(cache_url, resp)
+
+        # Add to the cache if the response headers demand it. If there
+        # is no date header then we can't do anything about expiring
+        # the cache.
+        elif 'date' in resp.headers:
+            # cache when there is a max-age > 0
+            if cc_resp and cc_resp.get('max-age'):
+                if int(cc_resp['max-age']) > 0:
+                    self.cache.set(cache_url, resp)
+
+            # If the request can expire, it means we should cache it
+            # in the meantime.
+            elif 'expires' in resp.headers:
+                if resp.headers['expires']:
+                    self.cache.set(cache_url, resp)
+
+    def update_cached_response(self, request, response):
+        """On a 304 we will get a new set of headers that we want to
+        update our cached value with, assuming we have one.
+
+        This should only ever be called when we've sent an ETag and
+        gotten a 304 as the response.
+        """
+        cache_url = self.cache_url(request.url)
+
+        resp = self.cache.get(cache_url)
+
+        if not resp:
+            # we didn't have a cached response
+            return response
+
+        # did so lets update our headers
+        resp.headers.update(response.headers)
+
+        # we want a 200 b/c we have content via the cache
+        request.status_code = 200
+
+        # update the request as it has the if-none-match header + any
+        # other headers that the server might have updated (ie Date,
+        # Cache-Control, Expires, etc.)
+        resp.request = request
+
+        # update our cache
+        self.cache.set(cache_url, resp)
+
+        # Let everyone know this was from the cache.
+        resp.from_cache = True
+
+        return resp
diff --git a/sima/lib/logger.py b/sima/lib/logger.py
index 05c675e..3b287e2 100644
--- a/sima/lib/logger.py
+++ b/sima/lib/logger.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-
 # Copyright (c) 2009, 2010, 2013, 2014 Jack Kaliko <kaliko at azylum.org>
 #
 #  This file is part of sima
@@ -29,7 +28,7 @@ import sys
 
 
 LOG_FORMATS = {
-        logging.DEBUG:  '{asctime} {filename: >11}:{lineno: <3} {levelname: <5}: {message}',
+        logging.DEBUG: '{asctime} {filename: >11}:{lineno: <3} {levelname: <7}: {message}',
         logging.INFO:  '{asctime} {levelname: <7}: {message}',
         #logging.DEBUG: '{asctime} {filename}:{lineno}({funcName}) '
                                  #'{levelname}: {message}',
diff --git a/sima/lib/meta.py b/sima/lib/meta.py
index d589051..4e481e6 100644
--- a/sima/lib/meta.py
+++ b/sima/lib/meta.py
@@ -1,9 +1,31 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+"""
+Defines some object to handle audio file metadata
+"""
 
 from .simastr import SimaStr
 from .track import Track
 
 class MetaException(Exception):
+    """Generic Meta Exception"""
     pass
 
 class NotSameArtist(MetaException):
@@ -11,6 +33,7 @@ class NotSameArtist(MetaException):
 
 
 class Meta:
+    """Generic Class for Meta object"""
 
     def __init__(self, **kwargs):
         self.name = None
diff --git a/sima/lib/player.py b/sima/lib/player.py
index 751e464..6b6d2cb 100644
--- a/sima/lib/player.py
+++ b/sima/lib/player.py
@@ -1,4 +1,22 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2009-2014 Jack Kaliko <jack at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
 
 # TODO:
 # Add decorator to filter through history?
@@ -11,7 +29,6 @@ import logging
 
 
 class Player(object):
-
     """Player interface to inherit from.
 
     When querying player music library for tracks, Player instance *must* return
diff --git a/sima/lib/plugin.py b/sima/lib/plugin.py
index 0e80ae5..5f70942 100644
--- a/sima/lib/plugin.py
+++ b/sima/lib/plugin.py
@@ -1,6 +1,27 @@
 # -*- coding: utf-8 -*-
-
-class Plugin():
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+"""
+Plugin object to derive from
+"""
+
+class Plugin:
     """
     First non-empty line of the docstring is used as description
     Rest of the docstring at your convenience.
@@ -37,7 +58,7 @@ class Plugin():
         """
         conf = self.__daemon.config
         for sec in conf.sections():
-            if sec.lower() == self.__class__.__name__.lower():
+            if sec == self.__class__.__name__.lower():
                 self.plugin_conf = conf[sec]
         #if self.plugin_conf:
         #    self.log.debug('Got config for {0}: {1}'.format(self,
@@ -81,6 +102,7 @@ class Plugin():
         pass
 
     def shutdown(self):
+        """Called on application shutdown"""
         pass
 
 
diff --git a/sima/lib/simadb.py b/sima/lib/simadb.py
index 284f115..430e9c3 100644
--- a/sima/lib/simadb.py
+++ b/sima/lib/simadb.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-
+#
 # Copyright (c) 2009-2013 Jack Kaliko <jack at azylum.org>
 # Copyright (c) 2009, Eric Casteleijn <thisfred at gmail.com>
 # Copyright (c) 2008 Rick van Hattem
@@ -20,6 +20,8 @@
 #  along with sima.  If not, see <http://www.gnu.org/licenses/>.
 #
 #
+"""SQlite database library
+"""
 
 #    DOC:
 #    MuscicBrainz ID: <http://musicbrainz.org/doc/MusicBrainzIdentifier>
@@ -27,7 +29,7 @@
 #             <http://musicbrainz.org/doc/Same_Artist_With_Different_Names>
 
 __DB_VERSION__ = 2
-__HIST_DURATION__ = int(7 * 24)  # in hours
+__HIST_DURATION__ = int(30 * 24)  # in hours
 
 import sqlite3
 
@@ -67,6 +69,7 @@ class SimaDB(object):
         self.db_path_mod_control()
 
     def db_path_mod_control(self):
+        """Controls DB path access & write permissions"""
         db_path = self._db_path
         # Controls directory access
         if not isdir(dirname(db_path)):
@@ -347,7 +350,7 @@ class SimaDB(object):
         """Retrieve complete play history, most recent tracks first
         artist  : filter history for specific artist
         artists : filter history for specific artists list
-        """
+        """ # pylint: disable=C0301
         date = datetime.utcnow() - timedelta(hours=duration)
         connection = self.get_database_connection()
         if artist:
@@ -380,7 +383,8 @@ class SimaDB(object):
         yield ('Row ID', 'Artist',)
         for row in rows:
             yield row
-        rows = connection.execute('SELECT black_list.rowid, albums.name, artists.name'
+        rows = connection.execute(
+                'SELECT black_list.rowid, albums.name, artists.name'
                 ' FROM artists, albums INNER JOIN black_list'
                 ' ON albums.id = black_list.album'
                 ' WHERE artists.id = albums.artist')
@@ -388,7 +392,8 @@ class SimaDB(object):
         yield ('Row ID', 'Album', 'Artist name')
         for row in rows:
             yield row
-        rows = connection.execute('SELECT black_list.rowid, tracks.name, artists.name'
+        rows = connection.execute(
+                'SELECT black_list.rowid, tracks.name, artists.name'
                 ' FROM artists, tracks INNER JOIN black_list'
                 ' ON tracks.id = black_list.track'
                 ' WHERE tracks.artist = artists.id')
@@ -522,9 +527,11 @@ class SimaDB(object):
         """Add to history"""
         connection = self.get_database_connection()
         track_id = self.get_track(track, with_connection=connection)[0]
-        rows = connection.execute("SELECT * FROM history WHERE track = ? ", (track_id,))
+        rows = connection.execute("SELECT * FROM history WHERE track = ? ",
+                                  (track_id,))
         if not rows.fetchone():
-            connection.execute("INSERT INTO history (track) VALUES (?)", (track_id,))
+            connection.execute("INSERT INTO history (track) VALUES (?)",
+                               (track_id,))
         connection.execute("UPDATE history SET last_play = DATETIME('now') "
                 " WHERE track = ?", (track_id,))
         connection.commit()
@@ -599,7 +606,7 @@ class SimaDB(object):
             "SELECT artist FROM albums")] +
             [row[0] for row in connection.execute(
             "SELECT artist FROM tracks")])
-        orphans = [ (orphan,) for orphan in artists_ids - artist_2_artist_ids ]
+        orphans = [(orphan,) for orphan in artists_ids - artist_2_artist_ids]
         connection.executemany('DELETE FROM artists WHERE id = (?);', orphans)
         if not with_connection:
             connection.commit()
@@ -619,7 +626,7 @@ class SimaDB(object):
             """SELECT albums.id FROM albums
             LEFT JOIN tracks ON albums.id = tracks.album
             WHERE tracks.album IS NULL""")])
-        orphans = [ (orphan,) for orphan in orphan_black_ids & orphan_tracks_ids ]
+        orphans = [(orphan,) for orphan in orphan_black_ids & orphan_tracks_ids]
         connection.executemany('DELETE FROM albums WHERE id = (?);', orphans)
         if not with_connection:
             connection.commit()
@@ -639,7 +646,7 @@ class SimaDB(object):
             """SELECT tracks.id FROM tracks
             LEFT JOIN black_list ON tracks.id = black_list.track
             WHERE black_list.track IS NULL""")])
-        orphans = [ (orphan,) for orphan in hist_orphan_ids & black_list_orphan_ids ]
+        orphans = [(orphan,) for orphan in hist_orphan_ids & black_list_orphan_ids]
         connection.executemany('DELETE FROM tracks WHERE id = (?);', orphans)
         if not with_connection:
             connection.commit()
@@ -668,6 +675,7 @@ class SimaDB(object):
         self.close_database_connection(connection)
 
     def _set_dbversion(self):
+        """Add db version"""
         connection = self.get_database_connection()
         connection.execute('INSERT INTO db_info (version, name) VALUES (?, ?)',
                 (__DB_VERSION__, 'Sima DB'))
@@ -704,13 +712,13 @@ class SimaDB(object):
             'CREATE TABLE IF NOT EXISTS history (last_play DATE,'
             ' track integer)')
         connection.execute(
-            "CREATE INDEX IF NOT EXISTS a2aa1x ON usr_artist_2_artist (artist1)")
+          "CREATE INDEX IF NOT EXISTS a2aa1x ON usr_artist_2_artist (artist1)")
         connection.execute(
-            "CREATE INDEX IF NOT EXISTS a2aa2x ON usr_artist_2_artist (artist2)")
+          "CREATE INDEX IF NOT EXISTS a2aa2x ON usr_artist_2_artist (artist2)")
         connection.execute(
-            "CREATE INDEX IF NOT EXISTS lfma2aa1x ON lfm_artist_2_artist (artist1)")
+        "CREATE INDEX IF NOT EXISTS lfma2aa1x ON lfm_artist_2_artist (artist1)")
         connection.execute(
-            "CREATE INDEX IF NOT EXISTS lfma2aa2x ON lfm_artist_2_artist (artist2)")
+        "CREATE INDEX IF NOT EXISTS lfma2aa2x ON lfm_artist_2_artist (artist2)")
         connection.commit()
         self.close_database_connection(connection)
         self._set_dbversion()
diff --git a/sima/lib/simaecho.py b/sima/lib/simaecho.py
index 716a43e..e5fe485 100644
--- a/sima/lib/simaecho.py
+++ b/sima/lib/simaecho.py
@@ -21,119 +21,122 @@
 Consume EchoNest web service
 """
 
-__version__ = '0.0.1'
+__version__ = '0.0.2'
 __author__ = 'Jack Kaliko'
 
 
-import logging
+from requests import Session, Request, Timeout, ConnectionError
 
-from datetime import datetime, timedelta
-from time import sleep
-
-from requests import get, Request, Timeout, ConnectionError
-
-from sima import ECH
+from sima import ECH, SOCKET_TIMEOUT, WAIT_BETWEEN_REQUESTS
 from sima.lib.meta import Artist
-from sima.utils.utils import getws, Throttle, Cache, purge_cache
+from sima.lib.track import Track
+from sima.lib.http import CacheController
+from sima.utils.utils import WSError, WSNotFound, WSTimeout, WSHTTPError
+from sima.utils.utils import getws, Throttle
 if len(ECH.get('apikey')) == 23:  # simple hack allowing imp.reload
     getws(ECH)
 
-# Some definitions
-WAIT_BETWEEN_REQUESTS = timedelta(0, 1)
-SOCKET_TIMEOUT = 4
-
-
-class EchoError(Exception):
-    pass
 
-class EchoNotFound(EchoError):
-    pass
-
-class EchoTimeout(EchoError):
-    pass
-
-class EchoHTTPError(EchoError):
-    pass
-
-class SimaEch():
-    """
+class SimaEch:
+    """EchoNest http client
     """
     root_url = 'http://{host}/api/{version}'.format(**ECH)
-    cache = {}
-    timestamp = datetime.utcnow()
     ratelimit = None
+    name = 'EchoNest'
+    cache = False
+    stats = {'etag':0,
+            'ccontrol':0,
+            'minrl':120,
+            'total':0}
 
-    def __init__(self, cache=True):
-        self.artist = None
-        self._ressource = None
-        self.current_element = None
-        self.caching = cache
-        purge_cache(self.__class__)
-
-    def _fetch(self, payload):
-        """Use cached elements or proceed http request"""
-        url = Request('GET', self._ressource, params=payload,).prepare().url
-        if url in SimaEch.cache:
-            self.current_element = SimaEch.cache.get(url).elem
-            return
+    def __init__(self):
+        self.controller = CacheController(self.cache)
+
+    def _fetch(self, ressource, payload):
+        """
+        Prepare http request
+        Use cached elements or proceed http request
+        """
+        req = Request('GET', ressource, params=payload,
+                      ).prepare()
+        SimaEch.stats.update(total=SimaEch.stats.get('total')+1)
+        if self.cache:
+            cached_response = self.controller.cached_request(req.url, req.headers)
+            if cached_response:
+                SimaEch.stats.update(ccontrol=SimaEch.stats.get('ccontrol')+1)
+                return cached_response.json()
         try:
-            self._fetch_ech(payload)
+            return self._fetch_ws(req)
         except Timeout:
-            raise EchoTimeout('Failed to reach server within {0}s'.format(
+            raise WSTimeout('Failed to reach server within {0}s'.format(
                                SOCKET_TIMEOUT))
         except ConnectionError as err:
-            raise EchoError(err)
+            raise WSError(err)
 
     @Throttle(WAIT_BETWEEN_REQUESTS)
-    def _fetch_ech(self, payload):
+    def _fetch_ws(self, prepreq):
         """fetch from web service"""
-        req = get(self._ressource, params=payload,
-                            timeout=SOCKET_TIMEOUT)
-        self.__class__.ratelimit = req.headers.get('x-ratelimit-remaining', None)
-        if req.status_code is not 200:
-            raise EchoHTTPError(req.status_code)
-        self.current_element = req.json()
-        self._controls_answer()
-        if self.caching:
-            SimaEch.cache.update({req.url:
-                                 Cache(self.current_element)})
-
-    def _controls_answer(self):
+        sess = Session()
+        resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
+        if resp.status_code == 304:
+            SimaEch.stats.update(etag=SimaEch.stats.get('etag')+1)
+            resp = self.controller.update_cached_response(prepreq, resp)
+        elif resp.status_code != 200:
+            raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
+        ans = resp.json()
+        self._controls_answer(ans)
+        SimaEch.ratelimit = resp.headers.get('x-ratelimit-remaining', None)
+        minrl = min(int(SimaEch.ratelimit), SimaEch.stats.get('minrl'))
+        SimaEch.stats.update(minrl=minrl)
+        if self.cache:
+            self.controller.cache_response(resp.request, resp)
+        return ans
+
+    def _controls_answer(self, ans):
         """Controls answer.
         """
-        status = self.current_element.get('response').get('status')
+        status = ans.get('response').get('status')
         code = status.get('code')
         if code is 0:
             return True
         if code is 5:
-            raise EchoNotFound('Artist not found: "{0}"'.format(self.artist))
-        raise EchoError(status.get('message'))
+            raise WSNotFound('Artist not found')
+        raise WSError(status.get('message'))
 
-    def _forge_payload(self, artist):
-        """
+    def _forge_payload(self, artist, top=False):
+        """Build payload
         """
         payload = {'api_key': ECH.get('apikey')}
         if not isinstance(artist, Artist):
             raise TypeError('"{0!r}" not an Artist object'.format(artist))
-        self.artist = artist
         if artist.mbid:
             payload.update(
                     id='musicbrainz:artist:{0}'.format(artist.mbid))
         else:
-           payload.update(name=artist.name)
+            payload.update(name=artist.name)
         payload.update(bucket='id:musicbrainz')
         payload.update(results=100)
-        return payload
+        if top:
+            if artist.mbid:
+                aid = payload.pop('id')
+                payload.update(artist_id=aid)
+            else:
+                name = payload.pop('name')
+                payload.update(artist=name)
+            payload.update(results=100)
+            payload.update(sort='song_hotttnesss-desc')
+        # > hashing the URL into a cache key
+        # return a sorted list of 2-tuple to have consistent cache
+        return sorted(payload.items(), key=lambda param: param[0])
 
     def get_similar(self, artist=None):
-        """
+        """Fetch similar artists
         """
         payload = self._forge_payload(artist)
         # Construct URL
-        self._ressource = '{0}/artist/similar'.format(SimaEch.root_url)
-        self._fetch(payload)
-        for art in self.current_element.get('response').get('artists'):
-            artist = {}
+        ressource = '{0}/artist/similar'.format(SimaEch.root_url)
+        ans = self._fetch(ressource, payload)
+        for art in ans.get('response').get('artists'):
             mbid = None
             if 'foreign_ids' in art:
                 for frgnid in art.get('foreign_ids'):
@@ -142,6 +145,24 @@ class SimaEch():
                                           ).lstrip('musicbrainz:artist:')
             yield Artist(mbid=mbid, name=art.get('name'))
 
+    def get_toptrack(self, artist=None):
+        """Fetch artist top tracks
+        """
+        payload = self._forge_payload(artist, top=True)
+        # Construct URL
+        ressource = '{0}/song/search'.format(SimaEch.root_url)
+        ans = self._fetch(ressource, payload)
+        titles = list()
+        art = {
+                'artist': artist.name,
+                'musicbrainz_artistid': artist.mbid,
+                }
+        for song in ans.get('response').get('songs'):
+            title = song.get('title')
+            if title not in titles:
+                titles.append(title)
+                yield Track(title=title, **art)
+
 
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/sima/lib/simafm.py b/sima/lib/simafm.py
index d01567e..0988266 100644
--- a/sima/lib/simafm.py
+++ b/sima/lib/simafm.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2014 Jack Kaliko <kaliko at azylum.org>
+# Copyright (c) 2009, 2010, 2011, 2012, 2013, 2014 Jack Kaliko <kaliko at azylum.org>
 #
 #   This program is free software: you can redistribute it and/or modify
 #   it under the terms of the GNU General Public License as published by
@@ -18,65 +18,57 @@
 #
 
 """
-Consume EchoNest web service
+Consume Last.fm web service
 """
 
-__version__ = '0.0.1'
+__version__ = '0.5.1'
 __author__ = 'Jack Kaliko'
 
 
-from datetime import datetime, timedelta
 
-from requests import get, Request, Timeout, ConnectionError
+from requests import Session, Request, Timeout, ConnectionError
 
-from sima import LFM
+from sima import LFM, SOCKET_TIMEOUT, WAIT_BETWEEN_REQUESTS
 from sima.lib.meta import Artist
-from sima.utils.utils import getws, Throttle, Cache, purge_cache
+from sima.lib.track import Track
+
+from sima.lib.http import CacheController
+from sima.utils.utils import WSError, WSNotFound, WSTimeout, WSHTTPError
+from sima.utils.utils import getws, Throttle
 if len(LFM.get('apikey')) == 43:  # simple hack allowing imp.reload
     getws(LFM)
 
-# Some definitions
-WAIT_BETWEEN_REQUESTS = timedelta(0, 1)
-SOCKET_TIMEOUT = 4
-
-
-class WSError(Exception):
-    pass
-
-class WSNotFound(WSError):
-    pass
-
-class WSTimeout(WSError):
-    pass
-
-class WSHTTPError(WSError):
-    pass
-
-
 
-class SimaFM():
-    """
+class SimaFM:
+    """Last.fm http client
     """
     root_url = 'http://{host}/{version}/'.format(**LFM)
-    cache = {}
-    timestamp = datetime.utcnow()
-    #ratelimit = None
-
-    def __init__(self, cache=True):
+    ratelimit = None
+    name = 'Last.fm'
+    cache = False
+    stats = {'etag':0,
+            'ccontrol':0,
+            'total':0}
+
+    def __init__(self):
+        self.controller = CacheController(self.cache)
         self.artist = None
-        self._url = self.__class__.root_url
-        self.current_element = None
-        self.caching = cache
-        purge_cache(self.__class__)
 
     def _fetch(self, payload):
-        """Use cached elements or proceed http request"""
-        url = Request('GET', self._url, params=payload,).prepare().url
-        if url in SimaFM.cache:
-            self.current_element = SimaFM.cache.get(url).elem
-            return
+        """
+        Prepare http request
+        Use cached elements or proceed http request
+        """
+        req = Request('GET', SimaFM.root_url, params=payload,
+                      ).prepare()
+        SimaFM.stats.update(total=SimaFM.stats.get('total')+1)
+        if self.cache:
+            cached_response = self.controller.cached_request(req.url, req.headers)
+            if cached_response:
+                SimaFM.stats.update(ccontrol=SimaFM.stats.get('ccontrol')+1)
+                return cached_response.json()
         try:
-            self._fetch_ech(payload)
+            return self._fetch_ws(req)
         except Timeout:
             raise WSTimeout('Failed to reach server within {0}s'.format(
                                SOCKET_TIMEOUT))
@@ -84,32 +76,34 @@ class SimaFM():
             raise WSError(err)
 
     @Throttle(WAIT_BETWEEN_REQUESTS)
-    def _fetch_ech(self, payload):
+    def _fetch_ws(self, prepreq):
         """fetch from web service"""
-        req = get(self._url, params=payload,
-                            timeout=SOCKET_TIMEOUT)
-        #self.__class__.ratelimit = req.headers.get('x-ratelimit-remaining', None)
-        if req.status_code is not 200:
-            raise WSHTTPError(req.status_code)
-        self.current_element = req.json()
-        self._controls_answer()
-        if self.caching:
-            SimaFM.cache.update({req.url:
-                                 Cache(self.current_element)})
-
-    def _controls_answer(self):
+        sess = Session()
+        resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
+        if resp.status_code == 304:
+            SimaFM.stats.update(etag=SimaFM.stats.get('etag')+1)
+            resp = self.controller.update_cached_response(prepreq, resp)
+        elif resp.status_code != 200:
+            raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
+        ans = resp.json()
+        self._controls_answer(ans)
+        if self.cache:
+            self.controller.cache_response(resp.request, resp)
+        return ans
+
+    def _controls_answer(self, ans):
         """Controls answer.
         """
-        if 'error' in self.current_element:
-            code = self.current_element.get('error')
-            mess = self.current_element.get('message')
+        if 'error' in ans:
+            code = ans.get('error')
+            mess = ans.get('message')
             if code == 6:
                 raise WSNotFound('{0}: "{1}"'.format(mess, self.artist))
             raise WSError(mess)
         return True
 
     def _forge_payload(self, artist, method='similar', track=None):
-        """
+        """Build payload
         """
         payloads = dict({'similar': {'method':'artist.getsimilar',},
                         'top': {'method':'artist.gettoptracks',},
@@ -124,23 +118,52 @@ class SimaFM():
         if artist.mbid:
             payload.update(mbid='{0}'.format(artist.mbid))
         else:
-           payload.update(artist=artist.name)
+            payload.update(artist=artist.name,
+                           autocorrect=1)
         payload.update(results=100)
         if method == 'track':
             payload.update(track=track)
-        return payload
+        # > hashing the URL into a cache key
+        # return a sorted list of 2-tuple to have consistent cache
+        return sorted(payload.items(), key=lambda param: param[0])
 
     def get_similar(self, artist=None):
-        """
+        """Fetch similar artists
         """
         payload = self._forge_payload(artist)
         # Construct URL
-        self._fetch(payload)
-        for art in self.current_element.get('similarartists').get('artist'):
-            match = 100 * float(art.get('match'))
-            yield Artist(mbid=art.get('mbid', None),
-                         name=art.get('name')), match
-
+        ans = self._fetch(payload)
+        # Artist might be found be return no 'artist' list…
+        # cf. "Mulatu Astatqe" vs. "Mulatu Astatqé" with autocorrect=0
+        # json format is broken IMHO, xml is more consistent IIRC
+        # Here what we got:
+        # >>> {"similarartists":{"#text":"\n","artist":"Mulatu Astatqe"}}
+        # autocorrect=1 should fix it, checking anyway.
+        simarts = ans.get('similarartists').get('artist')
+        if not isinstance(simarts, list):
+            raise WSError('Artist found but no similarities returned')
+        for art in ans.get('similarartists').get('artist'):
+            yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
+
+    def get_toptrack(self, artist=None):
+        """Fetch artist top tracks
+        """
+        payload = self._forge_payload(artist, method='top')
+        ans = self._fetch(payload)
+        tops = ans.get('toptracks').get('track')
+        art = {
+                'artist': artist.name,
+                'musicbrainz_artistid': artist.mbid,
+                }
+        for song in tops:
+            for key in ['artist', 'streamable', 'listeners',
+                        'url', 'image', '@attr']:
+                if key in song:
+                    song.pop(key)
+            song.update(art)
+            song.update(title=song.pop('name'))
+            song.update(time=song.pop('duration'))
+            yield Track(**song)
 
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/sima/lib/simastr.py b/sima/lib/simastr.py
index 9dacc45..56edbb4 100644
--- a/sima/lib/simastr.py
+++ b/sima/lib/simastr.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-
 #
 # Copyright (c) 2009, 2010, 2013 Jack Kaliko <kaliko at azylum.org>
 #
@@ -18,7 +17,7 @@
 #  If not, see <http://www.gnu.org/licenses/>.
 #
 
-"""
+r"""
 SimaStr
 
 Special unicode() subclass to perform fuzzy match on specific strings with
@@ -70,7 +69,7 @@ __version__ = '0.4'
 
 # IMPORTS
 import unicodedata
-from re import (compile, U, I)
+from re import compile as re_compile, U, I
 
 from ..utils.leven import levenshtein_ratio
 
@@ -94,11 +93,11 @@ class SimaStr(str):
     # Trailing patterns: ! ? live
     # TODO: add "concert" key word
     #       add "Live at <somewhere>"
-    regexp_dict.update({'trail': '([- !?\.]|\(? ?[Ll]ive ?\)?)'})
+    regexp_dict.update({'trail': r'([- !?\.]|\(? ?[Ll]ive ?\)?)'})
 
-    reg_lead = compile('^(?P<lead>%(lead)s )(?P<root0>.*)$' % regexp_dict, I | U)
-    reg_midl = compile('^(?P<root0>.*)(?P<mid> %(mid)s )(?P<root1>.*)' % regexp_dict, U)
-    reg_trail = compile('^(?P<root0>.*?)(?P<trail>%(trail)s+$)' % regexp_dict, U)
+    reg_lead = re_compile('^(?P<lead>%(lead)s )(?P<root0>.*)$' % regexp_dict, I | U)
+    reg_midl = re_compile('^(?P<root0>.*)(?P<mid> %(mid)s )(?P<root1>.*)' % regexp_dict, U)
+    reg_trail = re_compile('^(?P<root0>.*?)(?P<trail>%(trail)s+$)' % regexp_dict, U)
 
     def __init__(self, fuzzstr):
         """
@@ -108,7 +107,7 @@ class SimaStr(str):
         # fuzzy computation
         self._get_root()
         if self.__class__.diafilter:
-           self.remove_diacritics()
+            self.remove_diacritics()
 
     def __new__(cls, fuzzstr):
         return super(SimaStr, cls).__new__(cls, fuzzstr)
@@ -134,6 +133,7 @@ class SimaStr(str):
             self.stripped = sea.group('root0')
 
     def remove_diacritics(self):
+        """converting diacritics"""
         self.stripped = ''.join(x for x in
                                 unicodedata.normalize('NFKD', self.stripped)
                                 if unicodedata.category(x) != 'Mn')
diff --git a/sima/lib/track.py b/sima/lib/track.py
index 93904c8..5a01e17 100644
--- a/sima/lib/track.py
+++ b/sima/lib/track.py
@@ -24,7 +24,7 @@
 import time
 
 
-class Track(object):
+class Track:
     """
     Track object.
     Instanciate with Player replies.
@@ -37,7 +37,7 @@ class Track(object):
         self._file = file
         if not kwargs:
             self._empty = True
-        self.time = time
+        self._time = time
         self.__dict__.update(**kwargs)
         self.tags_to_collapse = ['artist', 'album', 'title', 'date',
                                  'genre', 'albumartist']
@@ -124,13 +124,5 @@ class Track(object):
             fmt = '%M:%S'
         return time.strftime(fmt, temps)
 
-
-def main():
-    pass
-
-# Script starts here
-if __name__ == '__main__':
-    main()
-
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/sima/plugins/internal/echonest.py b/sima/lib/webserv.py
similarity index 73%
copy from sima/plugins/internal/echonest.py
copy to sima/lib/webserv.py
index 4cd2f1d..75b0a38 100644
--- a/sima/plugins/internal/echonest.py
+++ b/sima/lib/webserv.py
@@ -1,6 +1,24 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2009-2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
 """
-Fetching similar artists from echonest web services
+Fetching similar artists from last.fm web services
 """
 
 # standard library import
@@ -12,11 +30,10 @@ from hashlib import md5
 # third parties components
 
 # local import
-from ...lib.plugin import Plugin
-from ...lib.simaecho import SimaEch, EchoError, EchoNotFound
-from ...lib.track import Track
-from ...lib.meta import Artist
-
+from .plugin import Plugin
+from .track import Track
+from .meta import Artist
+from ..utils.utils import WSError
 
 def cache(func):
     """Caching decorator"""
@@ -37,8 +54,8 @@ def cache(func):
     return wrapper
 
 
-class EchoNest(Plugin):
-    """Echonest autoqueue plugin http://the.echonest.com/
+class WebService(Plugin):
+    """similar artists webservice
     """
 
     def __init__(self, daemon):
@@ -56,6 +73,7 @@ class EchoNest(Plugin):
                 'album': self._album,
                 }
         self.queue_mode = wrapper.get(self.plugin_conf.get('queue_mode'))
+        self.ws = None
 
     def _flush_cache(self):
         """
@@ -74,7 +92,7 @@ class EchoNest(Plugin):
     def _cleanup_cache(self):
         """Avoid bloated cache
         """
-        for _ , val in self._cache.items():
+        for _, val in self._cache.items():
             if isinstance(val, dict):
                 while len(val) > 150:
                     val.popitem()
@@ -99,19 +117,11 @@ class EchoNest(Plugin):
         artist = tracks[0].artist
         black_list = self.player.queue + self.to_add
         not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
-        if not not_in_hist:
+        if self.plugin_conf.get('queue_mode') != 'top' and not not_in_hist:
             self.log.debug('All tracks already played for "{}"'.format(artist))
         random.shuffle(not_in_hist)
-        #candidate = [ trk for trk in not_in_hist if trk not in black_list
-                      #if not self.sdb.get_bl_track(trk, add_not=True)]
         candidate = []
         for trk in [_ for _ in not_in_hist if _ not in black_list]:
-            if self.sdb.get_bl_track(trk, add_not=True):
-                self.log.info('Blacklisted: {0}: '.format(trk))
-                continue
-            if self.sdb.get_bl_album(trk, add_not=True):
-                self.log.info('Blacklisted album: {0}: '.format(trk))
-                continue
             # Should use albumartist heuristic as well
             if self.plugin_conf.getboolean('single_album'):
                 if (trk.album == self.player.current.album or
@@ -121,10 +131,9 @@ class EchoNest(Plugin):
                     continue
             candidate.append(trk)
         if not candidate:
-            self.log.debug('Unable to find title to add' +
-                           ' for "%s".' % artist)
-            return None
+            return False
         self.to_add.append(random.choice(candidate))
+        return True
 
     def _get_artists_list_reorg(self, alist):
         """
@@ -139,10 +148,10 @@ class EchoNest(Plugin):
             if trk[0] not in art_in_hist:
                 art_in_hist.append(trk[0])
         art_in_hist.reverse()
-        art_not_in_hist = [ ar for ar in alist if ar not in art_in_hist ]
+        art_not_in_hist = [ar for ar in alist if ar not in art_in_hist]
         random.shuffle(art_not_in_hist)
         art_not_in_hist.extend(art_in_hist)
-        self.log.debug('history ordered: {}'.format(
+        self.log.info('{}'.format(
                        ' / '.join(art_not_in_hist)))
         return art_not_in_hist
 
@@ -152,20 +161,20 @@ class EchoNest(Plugin):
         Look in player library for availability of similar artists in
         similarities
         """
-        dynamic = self.plugin_conf.getint('dynamic')
+        dynamic = self.plugin_conf.getint('max_art')
         if dynamic <= 0:
             dynamic = 100
-        similarity = self.plugin_conf.getint('similarity')
         results = list()
+        similarities.reverse()
         while (len(results) < dynamic
             and len(similarities) > 0):
             art_pop = similarities.pop()
             results.extend(self.player.fuzzy_find_artist(art_pop))
         return results
 
-    def lfm_similar_artists(self, artist=None):
+    def ws_similar_artists(self, artist=None):
         """
-        Retrieve similar artists on echonest server.
+        Retrieve similar artists from WebServive.
         """
         if artist is None:
             curr = self.player.current.__dict__
@@ -174,22 +183,18 @@ class EchoNest(Plugin):
             current = Artist(name=name, mbid=mbid)
         else:
             current = artist
-        simaech = SimaEch()
         # initialize artists deque list to construct from DB
         as_art = deque()
-        as_artists = simaech.get_similar(artist=current)
-        self.log.debug('Requesting EchoNest for "{0}"'.format(current))
+        as_artists = self.ws().get_similar(artist=current)
+        self.log.debug('Requesting {1} for "{0}"'.format(current,
+                        self.ws.name))
         try:
             # TODO: let's propagate Artist type
             [as_art.append(str(art)) for art in as_artists]
-        except EchoNotFound as err:
-            self.log.warning(err)
-        except EchoError as err:
-            self.log.warning('EchoNest: {0}'.format(err))
+        except WSError as err:
+            self.log.warning('{0}: {1}'.format(self.ws.name, err))
         if as_art:
-            self.log.debug('Fetched  {0} artist(s) from echonest'.format(
-                            len(as_art)))
-        self.log.debug('x-ratelimit-remaining: {}'.format(SimaEch.ratelimit))
+            self.log.debug('Fetched {0} artist(s)'.format(len(as_art)))
         return as_art
 
     def get_recursive_similar_artist(self):
@@ -211,8 +216,10 @@ class EchoNest(Plugin):
         self.log.info('EXTRA ARTS: {}'.format(
             '/'.join([trk.artist for trk in extra_arts])))
         for artist in extra_arts:
-            self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
-            similar = self.lfm_similar_artists(artist=artist)
+            self.log.debug(
+                    'Looking for artist similar to "{0.artist}" as well'.format(
+                        artist))
+            similar = self.ws_similar_artists(artist=artist)
             if not similar:
                 return ret_extra
             ret_extra.extend(self.get_artists_from_player(similar))
@@ -221,16 +228,16 @@ class EchoNest(Plugin):
         return ret_extra
 
     def get_local_similar_artists(self):
-        """Check against local player for similar artists fetched from echonest
+        """Check against local player for similar artists
         """
         current = self.player.current
         self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
-        similar = list(self.lfm_similar_artists())
+        similar = self.ws_similar_artists()
         if not similar:
-            self.log.info('Got nothing from echonest!')
+            self.log.info('Got nothing from {0}!'.format(self.ws.name))
             return []
         self.log.info('First five similar artist(s): {}...'.format(
-                      ' / '.join([a for a in similar[0:5]])))
+                      ' / '.join([a for a in list(similar)[0:5]])))
         self.log.info('Looking availability in music library')
         ret = self.get_artists_from_player(similar)
         ret_extra = None
@@ -244,7 +251,6 @@ class EchoNest(Plugin):
             self.log.warning('Try running in debug mode to guess why...')
             return []
         self.log.info('Got {} artists in library'.format(len(ret)))
-        self.log.info(' / '.join(ret))
         # Move around similars items to get in unplayed|not recently played
         # artist first.
         return self._get_artists_list_reorg(ret)
@@ -267,9 +273,10 @@ class EchoNest(Plugin):
             self.log.info('Looking for an album to add for "%s"...' % artist)
             albums = self.player.find_albums(artist)
             # str conversion while Album type is not propagated
-            albums = [ str(album) for album in albums]
+            albums = [str(album) for album in albums]
             if albums:
-                self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
+                self.log.debug('Albums candidate: {0:s}'.format(
+                               ' / '.join(albums)))
             else: continue
             # albums yet in history for this artist
             albums = set(albums)
@@ -294,13 +301,39 @@ class EchoNest(Plugin):
             if not album_to_queue:
                 self.log.info('No album found for "%s"' % artist)
                 continue
-            self.log.info('echonest album candidates: {0} - {1}'.format(
-                           artist, album_to_queue))
+            self.log.info('{2} album candidate: {0} - {1}'.format(
+                           artist, album_to_queue, self.ws.name))
             nb_album_add += 1
             self.to_add.extend(self.player.find_album(artist, album_to_queue))
             if nb_album_add == target_album_to_add:
                 return True
 
+    def find_top(self, artists):
+        """
+        find top tracks for artists in artists list.
+        """
+        self.to_add = list()
+        nbtracks_target = self.plugin_conf.getint('track_to_add')
+        webserv = self.ws()
+        for artist in artists:
+            artist = Artist(name=artist)
+            if len(self.to_add) == nbtracks_target:
+                return True
+            self.log.info('Looking for a top track for {0}'.format(artist))
+            titles = deque()
+            try:
+                titles = [t for t in webserv.get_toptrack(artist)]
+            except WSError as err:
+                self.log.warning('{0}: {1}'.format(self.ws.name, err))
+            if self.ws.ratelimit:
+                self.log.info('{0.name} ratelimit: {0.ratelimit}'.format(self.ws))
+            for trk in titles:
+                found = self.player.fuzzy_find_track(artist.name, trk.title)
+                if found:
+                    self.log.debug('{0}'.format(found[0]))
+                    if self.filter_track(found):
+                        break
+
     def _track(self):
         """Get some tracks for track queue mode
         """
@@ -310,6 +343,9 @@ class EchoNest(Plugin):
             self.log.debug('Trying to find titles to add for "{}"'.format(
                            artist))
             found = self.player.find_track(artist)
+            if not found:
+                self.log.debug('Found nothing to queue for {0}'.format(artist))
+                continue
             # find tracks not in history for artist
             self.filter_track(found)
             if len(self.to_add) == nbtracks_target:
@@ -319,7 +355,7 @@ class EchoNest(Plugin):
                             'history getting too large?')
             return None
         for track in self.to_add:
-            self.log.info('echonest candidates: {0!s}'.format(track))
+            self.log.info('{1} candidates: {0!s}'.format(track, self.ws.name))
 
     def _album(self):
         """Get albums for album queue mode
@@ -330,8 +366,10 @@ class EchoNest(Plugin):
     def _top(self):
         """Get some tracks for top track queue mode
         """
-        #artists = self.get_local_similar_artists()
-        pass
+        artists = self.get_local_similar_artists()
+        self.find_top(artists)
+        for track in self.to_add:
+            self.log.info('{1} candidates: {0!s}'.format(track, self.ws.name))
 
     def callback_need_track(self):
         self._cleanup_cache()
@@ -343,6 +381,9 @@ class EchoNest(Plugin):
             self.log.debug(repr(self.player.current))
             return None
         self.queue_mode()
+        msg = ' '.join(['{0}: {1:>3d}'.format(k, v) for
+                        k, v in sorted(self.ws.stats.items())])
+        self.log.debug(msg)
         candidates = self.to_add
         self.to_add = list()
         if self.plugin_conf.get('queue_mode') != 'album':
diff --git a/sima/plugins/__init__.py b/sima/plugins/__init__.py
index e69de29..96e495e 100644
--- a/sima/plugins/__init__.py
+++ b/sima/plugins/__init__.py
@@ -0,0 +1 @@
+# pylint: disable=missing-docstring
diff --git a/sima/plugins/contrib/__init__.py b/sima/plugins/contrib/__init__.py
index e69de29..96e495e 100644
--- a/sima/plugins/contrib/__init__.py
+++ b/sima/plugins/contrib/__init__.py
@@ -0,0 +1 @@
+# pylint: disable=missing-docstring
diff --git a/sima/plugins/contrib/placeholder.py b/sima/plugins/contrib/placeholder.py
index 60e34b5..f70474b 100644
--- a/sima/plugins/contrib/placeholder.py
+++ b/sima/plugins/contrib/placeholder.py
@@ -1,9 +1,26 @@
 # -*- coding: utf-8 -*-
-"""Crops playlist
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+"""PlaceHolder
 """
 
 # standard library import
-#from select import select
 
 # third parties components
 
diff --git a/sima/plugins/core/__init__.py b/sima/plugins/core/__init__.py
index e69de29..96e495e 100644
--- a/sima/plugins/core/__init__.py
+++ b/sima/plugins/core/__init__.py
@@ -0,0 +1 @@
+# pylint: disable=missing-docstring
diff --git a/sima/plugins/core/history.py b/sima/plugins/core/history.py
index 67414a8..b791053 100644
--- a/sima/plugins/core/history.py
+++ b/sima/plugins/core/history.py
@@ -1,4 +1,22 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
 """Add playing tracks to history
 """
 
diff --git a/sima/plugins/core/mpdoptions.py b/sima/plugins/core/mpdoptions.py
index be47a40..1c4f852 100644
--- a/sima/plugins/core/mpdoptions.py
+++ b/sima/plugins/core/mpdoptions.py
@@ -1,4 +1,22 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
 """
     Deal with MPD options ‑ idle and repeat mode
 """
diff --git a/sima/plugins/internal/__init__.py b/sima/plugins/internal/__init__.py
index e69de29..96e495e 100644
--- a/sima/plugins/internal/__init__.py
+++ b/sima/plugins/internal/__init__.py
@@ -0,0 +1 @@
+# pylint: disable=missing-docstring
diff --git a/sima/plugins/internal/crop.py b/sima/plugins/internal/crop.py
index 2ecbfc1..1f8ab2c 100644
--- a/sima/plugins/internal/crop.py
+++ b/sima/plugins/internal/crop.py
@@ -1,4 +1,22 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
 """Crops playlist
 """
 
diff --git a/sima/plugins/internal/echonest.py b/sima/plugins/internal/echonest.py
index 4cd2f1d..c9a02e8 100644
--- a/sima/plugins/internal/echonest.py
+++ b/sima/plugins/internal/echonest.py
@@ -1,356 +1,47 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
 """
 Fetching similar artists from echonest web services
 """
 
 # standard library import
-import random
-
-from collections import deque
-from hashlib import md5
+from os.path import join
 
 # third parties components
 
 # local import
-from ...lib.plugin import Plugin
-from ...lib.simaecho import SimaEch, EchoError, EchoNotFound
-from ...lib.track import Track
-from ...lib.meta import Artist
-
-
-def cache(func):
-    """Caching decorator"""
-    def wrapper(*args, **kwargs):
-        #pylint: disable=W0212,C0111
-        cls = args[0]
-        similarities = [art for art in args[1]]
-        hashedlst = md5(''.join(similarities).encode('utf-8')).hexdigest()
-        if hashedlst in cls._cache.get('asearch'):
-            cls.log.debug('cached request')
-            results = cls._cache.get('asearch').get(hashedlst)
-        else:
-            results = func(*args, **kwargs)
-            cls.log.debug('caching request')
-            cls._cache.get('asearch').update({hashedlst:list(results)})
-        random.shuffle(results)
-        return results
-    return wrapper
+from ...lib.simaecho import SimaEch
+from ...lib.webserv import WebService
+from ...lib.cache import FileCache
 
 
-class EchoNest(Plugin):
-    """Echonest autoqueue plugin http://the.echonest.com/
+class EchoNest(WebService):
+    """last.fm similar artists
     """
 
     def __init__(self, daemon):
-        Plugin.__init__(self, daemon)
-        self.daemon_conf = daemon.config
-        self.sdb = daemon.sdb
-        self.history = daemon.short_history
-        ##
-        self.to_add = list()
-        self._cache = None
-        self._flush_cache()
-        wrapper = {
-                'track': self._track,
-                'top': self._top,
-                'album': self._album,
-                }
-        self.queue_mode = wrapper.get(self.plugin_conf.get('queue_mode'))
-
-    def _flush_cache(self):
-        """
-        Both flushes and instanciates _cache
-        """
-        name = self.__class__.__name__
-        if isinstance(self._cache, dict):
-            self.log.info('{0}: Flushing cache!'.format(name))
-        else:
-            self.log.info('{0}: Initialising cache!'.format(name))
-        self._cache = {
-                'asearch': dict(),
-                'tsearch': dict(),
-                }
-
-    def _cleanup_cache(self):
-        """Avoid bloated cache
-        """
-        for _ , val in self._cache.items():
-            if isinstance(val, dict):
-                while len(val) > 150:
-                    val.popitem()
-
-    def get_history(self, artist):
-        """Constructs list of Track for already played titles for an artist.
-        """
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        tracks_from_db = self.sdb.get_history(duration=duration, artist=artist)
-        # Construct Track() objects list from database history
-        played_tracks = [Track(artist=tr[-1], album=tr[1], title=tr[2],
-                               file=tr[3]) for tr in tracks_from_db]
-        return played_tracks
-
-    def filter_track(self, tracks):
-        """
-        Extract one unplayed track from a Track object list.
-            * not in history
-            * not already in the queue
-            * not blacklisted
-        """
-        artist = tracks[0].artist
-        black_list = self.player.queue + self.to_add
-        not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
-        if not not_in_hist:
-            self.log.debug('All tracks already played for "{}"'.format(artist))
-        random.shuffle(not_in_hist)
-        #candidate = [ trk for trk in not_in_hist if trk not in black_list
-                      #if not self.sdb.get_bl_track(trk, add_not=True)]
-        candidate = []
-        for trk in [_ for _ in not_in_hist if _ not in black_list]:
-            if self.sdb.get_bl_track(trk, add_not=True):
-                self.log.info('Blacklisted: {0}: '.format(trk))
-                continue
-            if self.sdb.get_bl_album(trk, add_not=True):
-                self.log.info('Blacklisted album: {0}: '.format(trk))
-                continue
-            # Should use albumartist heuristic as well
-            if self.plugin_conf.getboolean('single_album'):
-                if (trk.album == self.player.current.album or
-                    trk.album in [tr.album for tr in self.to_add]):
-                    self.log.debug('Found unplayed track ' +
-                               'but from an album already queued: %s' % (trk))
-                    continue
-            candidate.append(trk)
-        if not candidate:
-            self.log.debug('Unable to find title to add' +
-                           ' for "%s".' % artist)
-            return None
-        self.to_add.append(random.choice(candidate))
-
-    def _get_artists_list_reorg(self, alist):
-        """
-        Move around items in artists_list in order to play first not recently
-        played artists
-        """
-        # TODO: move to utils as a decorator
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        art_in_hist = list()
-        for trk in self.sdb.get_history(duration=duration,
-                                        artists=alist):
-            if trk[0] not in art_in_hist:
-                art_in_hist.append(trk[0])
-        art_in_hist.reverse()
-        art_not_in_hist = [ ar for ar in alist if ar not in art_in_hist ]
-        random.shuffle(art_not_in_hist)
-        art_not_in_hist.extend(art_in_hist)
-        self.log.debug('history ordered: {}'.format(
-                       ' / '.join(art_not_in_hist)))
-        return art_not_in_hist
-
-    @cache
-    def get_artists_from_player(self, similarities):
-        """
-        Look in player library for availability of similar artists in
-        similarities
-        """
-        dynamic = self.plugin_conf.getint('dynamic')
-        if dynamic <= 0:
-            dynamic = 100
-        similarity = self.plugin_conf.getint('similarity')
-        results = list()
-        while (len(results) < dynamic
-            and len(similarities) > 0):
-            art_pop = similarities.pop()
-            results.extend(self.player.fuzzy_find_artist(art_pop))
-        return results
-
-    def lfm_similar_artists(self, artist=None):
-        """
-        Retrieve similar artists on echonest server.
-        """
-        if artist is None:
-            curr = self.player.current.__dict__
-            name = curr.get('artist')
-            mbid = curr.get('musicbrainz_artistid', None)
-            current = Artist(name=name, mbid=mbid)
-        else:
-            current = artist
-        simaech = SimaEch()
-        # initialize artists deque list to construct from DB
-        as_art = deque()
-        as_artists = simaech.get_similar(artist=current)
-        self.log.debug('Requesting EchoNest for "{0}"'.format(current))
-        try:
-            # TODO: let's propagate Artist type
-            [as_art.append(str(art)) for art in as_artists]
-        except EchoNotFound as err:
-            self.log.warning(err)
-        except EchoError as err:
-            self.log.warning('EchoNest: {0}'.format(err))
-        if as_art:
-            self.log.debug('Fetched  {0} artist(s) from echonest'.format(
-                            len(as_art)))
-        self.log.debug('x-ratelimit-remaining: {}'.format(SimaEch.ratelimit))
-        return as_art
-
-    def get_recursive_similar_artist(self):
-        ret_extra = list()
-        history = deque(self.history)
-        history.popleft()
-        depth = 0
-        current = self.player.current
-        extra_arts = list()
-        while depth < self.plugin_conf.getint('depth'):
-            if len(history) == 0:
-                break
-            trk = history.popleft()
-            if (trk.artist in [trk.artist for trk in extra_arts]
-                or trk.artist == current.artist):
-                continue
-            extra_arts.append(trk)
-            depth += 1
-        self.log.info('EXTRA ARTS: {}'.format(
-            '/'.join([trk.artist for trk in extra_arts])))
-        for artist in extra_arts:
-            self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
-            similar = self.lfm_similar_artists(artist=artist)
-            if not similar:
-                return ret_extra
-            ret_extra.extend(self.get_artists_from_player(similar))
-            if current.artist in ret_extra:
-                ret_extra.remove(current.artist)
-        return ret_extra
-
-    def get_local_similar_artists(self):
-        """Check against local player for similar artists fetched from echonest
-        """
-        current = self.player.current
-        self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
-        similar = list(self.lfm_similar_artists())
-        if not similar:
-            self.log.info('Got nothing from echonest!')
-            return []
-        self.log.info('First five similar artist(s): {}...'.format(
-                      ' / '.join([a for a in similar[0:5]])))
-        self.log.info('Looking availability in music library')
-        ret = self.get_artists_from_player(similar)
-        ret_extra = None
-        if len(self.history) >= 2:
-            if self.plugin_conf.getint('depth') > 1:
-                ret_extra = self.get_recursive_similar_artist()
-        if ret_extra:
-            ret = list(set(ret) | set(ret_extra))
-        if not ret:
-            self.log.warning('Got nothing from music library.')
-            self.log.warning('Try running in debug mode to guess why...')
-            return []
-        self.log.info('Got {} artists in library'.format(len(ret)))
-        self.log.info(' / '.join(ret))
-        # Move around similars items to get in unplayed|not recently played
-        # artist first.
-        return self._get_artists_list_reorg(ret)
-
-    def _get_album_history(self, artist=None):
-        """Retrieve album history"""
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        albums_list = set()
-        for trk in self.sdb.get_history(artist=artist, duration=duration):
-            albums_list.add(trk[1])
-        return albums_list
-
-    def find_album(self, artists):
-        """Find albums to queue.
-        """
-        self.to_add = list()
-        nb_album_add = 0
-        target_album_to_add = self.plugin_conf.getint('album_to_add')
-        for artist in artists:
-            self.log.info('Looking for an album to add for "%s"...' % artist)
-            albums = self.player.find_albums(artist)
-            # str conversion while Album type is not propagated
-            albums = [ str(album) for album in albums]
-            if albums:
-                self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
-            else: continue
-            # albums yet in history for this artist
-            albums = set(albums)
-            albums_yet_in_hist = albums & self._get_album_history(artist=artist)
-            albums_not_in_hist = list(albums - albums_yet_in_hist)
-            # Get to next artist if there are no unplayed albums
-            if not albums_not_in_hist:
-                self.log.info('No album found for "%s"' % artist)
-                continue
-            album_to_queue = str()
-            random.shuffle(albums_not_in_hist)
-            for album in albums_not_in_hist:
-                tracks = self.player.find_album(artist, album)
-                # Look if one track of the album is already queued
-                # Good heuristic, at least enough to guess if the whole album is
-                # already queued.
-                if tracks[0] in self.player.queue:
-                    self.log.debug('"%s" already queued, skipping!' %
-                            tracks[0].album)
-                    continue
-                album_to_queue = album
-            if not album_to_queue:
-                self.log.info('No album found for "%s"' % artist)
-                continue
-            self.log.info('echonest album candidates: {0} - {1}'.format(
-                           artist, album_to_queue))
-            nb_album_add += 1
-            self.to_add.extend(self.player.find_album(artist, album_to_queue))
-            if nb_album_add == target_album_to_add:
-                return True
-
-    def _track(self):
-        """Get some tracks for track queue mode
-        """
-        artists = self.get_local_similar_artists()
-        nbtracks_target = self.plugin_conf.getint('track_to_add')
-        for artist in artists:
-            self.log.debug('Trying to find titles to add for "{}"'.format(
-                           artist))
-            found = self.player.find_track(artist)
-            # find tracks not in history for artist
-            self.filter_track(found)
-            if len(self.to_add) == nbtracks_target:
-                break
-        if not self.to_add:
-            self.log.debug('Found no tracks to queue, is your ' +
-                            'history getting too large?')
-            return None
-        for track in self.to_add:
-            self.log.info('echonest candidates: {0!s}'.format(track))
-
-    def _album(self):
-        """Get albums for album queue mode
-        """
-        artists = self.get_local_similar_artists()
-        self.find_album(artists)
-
-    def _top(self):
-        """Get some tracks for top track queue mode
-        """
-        #artists = self.get_local_similar_artists()
-        pass
-
-    def callback_need_track(self):
-        self._cleanup_cache()
-        if not self.player.current:
-            self.log.info('No current track, cannot queue')
-            return None
-        if not self.player.current.artist:
-            self.log.warning('No artist set for the current track')
-            self.log.debug(repr(self.player.current))
-            return None
-        self.queue_mode()
-        candidates = self.to_add
-        self.to_add = list()
-        if self.plugin_conf.get('queue_mode') != 'album':
-            random.shuffle(candidates)
-        return candidates
-
-    def callback_player_database(self):
-        self._flush_cache()
+        WebService.__init__(self, daemon)
+        # Set persitent cache
+        vardir = daemon.config['sima']['var_dir']
+        SimaEch.cache = FileCache(join(vardir, 'http', 'EchoNest'))
+        self.ws = SimaEch
 
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/sima/plugins/internal/lastfm.py b/sima/plugins/internal/lastfm.py
index 0cb3532..be87094 100644
--- a/sima/plugins/internal/lastfm.py
+++ b/sima/plugins/internal/lastfm.py
@@ -1,362 +1,52 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
 """
 Fetching similar artists from last.fm web services
 """
 
 # standard library import
-import random
-
-from collections import deque
-from hashlib import md5
+from os.path import join
 
 # third parties components
 
 # local import
-from ...lib.plugin import Plugin
-from ...lib.simafm import SimaFM, WSHTTPError, WSNotFound, WSError
-from ...lib.track import Track
-from ...lib.meta import Artist
-
+from ...lib.simafm import SimaFM
+from ...lib.webserv import WebService
+from ...lib.cache import FileCache, DictCache
 
-def cache(func):
-    """Caching decorator"""
-    def wrapper(*args, **kwargs):
-        #pylint: disable=W0212,C0111
-        cls = args[0]
-        similarities = [art for art, _ in args[1]]
-        hashedlst = md5(''.join(similarities).encode('utf-8')).hexdigest()
-        if hashedlst in cls._cache.get('asearch'):
-            cls.log.debug('cached request')
-            results = cls._cache.get('asearch').get(hashedlst)
-        else:
-            results = func(*args, **kwargs)
-            cls.log.debug('caching request')
-            cls._cache.get('asearch').update({hashedlst:list(results)})
-        random.shuffle(results)
-        return results
-    return wrapper
 
-
-class Lastfm(Plugin):
+class Lastfm(WebService):
     """last.fm similar artists
     """
 
     def __init__(self, daemon):
-        Plugin.__init__(self, daemon)
-        self.daemon_conf = daemon.config
-        self.sdb = daemon.sdb
-        self.history = daemon.short_history
-        ##
-        self.to_add = list()
-        self._cache = None
-        self._flush_cache()
-        wrapper = {
-                'track': self._track,
-                'top': self._top,
-                'album': self._album,
-                }
-        self.queue_mode = wrapper.get(self.plugin_conf.get('queue_mode'))
-
-    def _flush_cache(self):
-        """
-        Both flushes and instanciates _cache
-        """
-        name = self.__class__.__name__
-        if isinstance(self._cache, dict):
-            self.log.info('{0}: Flushing cache!'.format(name))
-        else:
-            self.log.info('{0}: Initialising cache!'.format(name))
-        self._cache = {
-                'asearch': dict(),
-                'tsearch': dict(),
-                }
-
-    def _cleanup_cache(self):
-        """Avoid bloated cache
-        """
-        for _ , val in self._cache.items():
-            if isinstance(val, dict):
-                while len(val) > 150:
-                    val.popitem()
-
-    def get_history(self, artist):
-        """Constructs list of Track for already played titles for an artist.
-        """
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        tracks_from_db = self.sdb.get_history(duration=duration, artist=artist)
-        # Construct Track() objects list from database history
-        played_tracks = [Track(artist=tr[-1], album=tr[1], title=tr[2],
-                               file=tr[3]) for tr in tracks_from_db]
-        return played_tracks
-
-    def filter_track(self, tracks):
-        """
-        Extract one unplayed track from a Track object list.
-            * not in history
-            * not already in the queue
-            * not blacklisted
-        """
-        artist = tracks[0].artist
-        black_list = self.player.queue + self.to_add
-        not_in_hist = list(set(tracks) - set(self.get_history(artist=artist)))
-        if not not_in_hist:
-            self.log.debug('All tracks already played for "{}"'.format(artist))
-        random.shuffle(not_in_hist)
-        #candidate = [ trk for trk in not_in_hist if trk not in black_list
-                      #if not self.sdb.get_bl_track(trk, add_not=True)]
-        candidate = []
-        for trk in [_ for _ in not_in_hist if _ not in black_list]:
-            if self.sdb.get_bl_track(trk, add_not=True):
-                self.log.info('Blacklisted: {0}: '.format(trk))
-                continue
-            if self.sdb.get_bl_album(trk, add_not=True):
-                self.log.info('Blacklisted album: {0}: '.format(trk))
-                continue
-            # Should use albumartist heuristic as well
-            if self.plugin_conf.getboolean('single_album'):
-                if (trk.album == self.player.current.album or
-                    trk.album in [tr.album for tr in self.to_add]):
-                    self.log.debug('Found unplayed track ' +
-                               'but from an album already queued: %s' % (trk))
-                    continue
-            candidate.append(trk)
-        if not candidate:
-            self.log.debug('Unable to find title to add' +
-                           ' for "%s".' % artist)
-            return None
-        self.to_add.append(random.choice(candidate))
-
-    def _get_artists_list_reorg(self, alist):
-        """
-        Move around items in artists_list in order to play first not recently
-        played artists
-        """
-        # TODO: move to utils as a decorator
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        art_in_hist = list()
-        for trk in self.sdb.get_history(duration=duration,
-                                        artists=alist):
-            if trk[0] not in art_in_hist:
-                art_in_hist.append(trk[0])
-        art_in_hist.reverse()
-        art_not_in_hist = [ ar for ar in alist if ar not in art_in_hist ]
-        random.shuffle(art_not_in_hist)
-        art_not_in_hist.extend(art_in_hist)
-        self.log.debug('history ordered: {}'.format(
-                       ' / '.join(art_not_in_hist)))
-        return art_not_in_hist
-
-    @cache
-    def get_artists_from_player(self, similarities):
-        """
-        Look in player library for availability of similar artists in
-        similarities
-        """
-        dynamic = self.plugin_conf.getint('dynamic')
-        if dynamic <= 0:
-            dynamic = 100
-        similarity = self.plugin_conf.getint('similarity')
-        results = list()
-        similarities.reverse()
-        while (len(results) < dynamic
-            and len(similarities) > 0):
-            art_pop, match = similarities.pop()
-            if match < similarity:
-                break
-            results.extend(self.player.fuzzy_find_artist(art_pop))
-        results and self.log.debug('Similarity: %d%%' % match) # pylint: disable=w0106
-        return results
-
-    def lfm_similar_artists(self, artist=None):
-        """
-        Retrieve similar artists on last.fm server.
-        """
-        if artist is None:
-            curr = self.player.current.__dict__
-            name = curr.get('artist')
-            mbid = curr.get('musicbrainz_artistid', None)
-            current = Artist(name=name, mbid=mbid)
+        WebService.__init__(self, daemon)
+        self.ws = SimaFM
+        # Set persitent cache
+        vardir = daemon.config['sima']['var_dir']
+        persitent_cache = daemon.config.getboolean('lastfm', 'cache')
+        if persitent_cache:
+            SimaFM.cache = FileCache(join(vardir, 'http', 'LastFM'))
         else:
-            current = artist
-        simafm = SimaFM()
-        # initialize artists deque list to construct from DB
-        as_art = deque()
-        as_artists = simafm.get_similar(artist=current)
-        self.log.debug('Requesting last.fm for "{0}"'.format(current))
-        try:
-            # TODO: let's propagate Artist type
-            [as_art.append((str(a), m)) for a, m in as_artists]
-        except WSHTTPError as err:
-            self.log.warning('last.fm http error: %s' % err)
-        except WSNotFound as err:
-            self.log.warning("last.fm: %s" % err)
-        except WSError as err:
-            self.log.warning('last.fm module error: %s' % err)
-        if as_art:
-            self.log.debug('Fetched %d artist(s) from last.fm' % len(as_art))
-        return as_art
-
-    def get_recursive_similar_artist(self):
-        ret_extra = list()
-        history = deque(self.history)
-        history.popleft()
-        depth = 0
-        current = self.player.current
-        extra_arts = list()
-        while depth < self.plugin_conf.getint('depth'):
-            if len(history) == 0:
-                break
-            trk = history.popleft()
-            if (trk.artist in [trk.artist for trk in extra_arts]
-                or trk.artist == current.artist):
-                continue
-            extra_arts.append(trk)
-            depth += 1
-        self.log.info('EXTRA ARTS: {}'.format(
-            '/'.join([trk.artist for trk in extra_arts])))
-        for artist in extra_arts:
-            self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
-            similar = self.lfm_similar_artists(artist=artist)
-            if not similar:
-                return ret_extra
-            similar = sorted(similar, key=lambda sim: sim[1], reverse=True)
-            ret_extra.extend(self.get_artists_from_player(similar))
-            if current.artist in ret_extra:
-                ret_extra.remove(current.artist)
-        return ret_extra
-
-    def get_local_similar_artists(self):
-        """Check against local player for similar artists fetched from last.fm
-        """
-        current = self.player.current
-        self.log.info('Looking for artist similar to "{0.artist}"'.format(current))
-        similar = self.lfm_similar_artists()
-        if not similar:
-            self.log.info('Got nothing from last.fm!')
-            return []
-        similar = sorted(similar, key=lambda sim: sim[1], reverse=True)
-        self.log.info('First five similar artist(s): {}...'.format(
-                      ' / '.join([a for a, _ in similar[0:5]])))
-        self.log.info('Looking availability in music library')
-        ret = self.get_artists_from_player(similar)
-        ret_extra = None
-        if len(self.history) >= 2:
-            if self.plugin_conf.getint('depth') > 1:
-                ret_extra = self.get_recursive_similar_artist()
-        if ret_extra:
-            ret = list(set(ret) | set(ret_extra))
-        if not ret:
-            self.log.warning('Got nothing from music library.')
-            self.log.warning('Try running in debug mode to guess why...')
-            return []
-        self.log.info('Got {} artists in library'.format(len(ret)))
-        self.log.info(' / '.join(ret))
-        # Move around similars items to get in unplayed|not recently played
-        # artist first.
-        return self._get_artists_list_reorg(ret)
-
-    def _get_album_history(self, artist=None):
-        """Retrieve album history"""
-        duration = self.daemon_conf.getint('sima', 'history_duration')
-        albums_list = set()
-        for trk in self.sdb.get_history(artist=artist, duration=duration):
-            albums_list.add(trk[1])
-        return albums_list
-
-    def find_album(self, artists):
-        """Find albums to queue.
-        """
-        self.to_add = list()
-        nb_album_add = 0
-        target_album_to_add = self.plugin_conf.getint('album_to_add')
-        for artist in artists:
-            self.log.info('Looking for an album to add for "%s"...' % artist)
-            albums = self.player.find_albums(artist)
-            # str conversion while Album type is not propagated
-            albums = [ str(album) for album in albums]
-            if albums:
-                self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
-            else: continue
-            # albums yet in history for this artist
-            albums = set(albums)
-            albums_yet_in_hist = albums & self._get_album_history(artist=artist)
-            albums_not_in_hist = list(albums - albums_yet_in_hist)
-            # Get to next artist if there are no unplayed albums
-            if not albums_not_in_hist:
-                self.log.info('No album found for "%s"' % artist)
-                continue
-            album_to_queue = str()
-            random.shuffle(albums_not_in_hist)
-            for album in albums_not_in_hist:
-                tracks = self.player.find_album(artist, album)
-                # Look if one track of the album is already queued
-                # Good heuristic, at least enough to guess if the whole album is
-                # already queued.
-                if tracks[0] in self.player.queue:
-                    self.log.debug('"%s" already queued, skipping!' %
-                            tracks[0].album)
-                    continue
-                album_to_queue = album
-            if not album_to_queue:
-                self.log.info('No album found for "%s"' % artist)
-                continue
-            self.log.info('last.fm album candidate: {0} - {1}'.format(
-                           artist, album_to_queue))
-            nb_album_add += 1
-            self.to_add.extend(self.player.find_album(artist, album_to_queue))
-            if nb_album_add == target_album_to_add:
-                return True
-
-    def _track(self):
-        """Get some tracks for track queue mode
-        """
-        artists = self.get_local_similar_artists()
-        nbtracks_target = self.plugin_conf.getint('track_to_add')
-        for artist in artists:
-            self.log.debug('Trying to find titles to add for "{}"'.format(
-                           artist))
-            found = self.player.find_track(artist)
-            # find tracks not in history for artist
-            self.filter_track(found)
-            if len(self.to_add) == nbtracks_target:
-                break
-        if not self.to_add:
-            self.log.debug('Found no tracks to queue, is your ' +
-                            'history getting too large?')
-            return None
-        for track in self.to_add:
-            self.log.info('last.fm candidates: {0!s}'.format(track))
-
-    def _album(self):
-        """Get albums for album queue mode
-        """
-        artists = self.get_local_similar_artists()
-        self.find_album(artists)
-
-    def _top(self):
-        """Get some tracks for top track queue mode
-        """
-        #artists = self.get_local_similar_artists()
-        pass
-
-    def callback_need_track(self):
-        self._cleanup_cache()
-        if not self.player.current:
-            self.log.info('No current track, cannot queue')
-            return None
-        if not self.player.current.artist:
-            self.log.warning('No artist set for the current track')
-            self.log.debug(repr(self.player.current))
-            return None
-        self.queue_mode()
-        candidates = self.to_add
-        self.to_add = list()
-        if self.plugin_conf.get('queue_mode') != 'album':
-            random.shuffle(candidates)
-        return candidates
+            SimaFM.cache = DictCache()
 
-    def callback_player_database(self):
-        self._flush_cache()
 
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/sima/plugins/internal/randomfallback.py b/sima/plugins/internal/randomfallback.py
index 07201c0..aceff44 100644
--- a/sima/plugins/internal/randomfallback.py
+++ b/sima/plugins/internal/randomfallback.py
@@ -1,4 +1,22 @@
 # -*- coding: utf-8 -*-
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#  This file is part of sima
+#
+#  sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
 """
 Fetching similar artists from last.fm web services
 """
@@ -10,10 +28,13 @@ import random
 
 # local import
 from ...lib.plugin import Plugin
-from ...lib.track import Track
 
 
 class RandomFallBack(Plugin):
+    """Add random track as fallback
+    TODO: refactor, this plugin does not look good to me.
+          callback_need_track_fb/get_trk articulation is not elegant at all
+    """
 
     def __init__(self, daemon):
         super().__init__(daemon)
@@ -21,9 +42,9 @@ class RandomFallBack(Plugin):
         if not self.plugin_conf:
             return
         self.mode = self.plugin_conf.get('flavour', None)
-        if self.mode not in ['pure', 'sensible', 'genre']:
+        if self.mode not in ['pure', 'sensible']:
             self.log.warning('Bad value for flavour, '
-                    '{} not in ["pure", "sensible", "genre"]'.format(self.mode))
+                    '"{}" not in ["pure", "sensible"]'.format(self.mode))
             self.mode = 'pure'
 
     def get_played_artist(self,):
@@ -32,21 +53,46 @@ class RandomFallBack(Plugin):
         duration = self.daemon.config.getint('sima', 'history_duration')
         tracks_from_db = self.daemon.sdb.get_history(duration=duration)
         # Construct Track() objects list from database history
-        artists = [ tr[-1] for tr in tracks_from_db ]
+        artists = [tr[-1] for tr in tracks_from_db]
         return set(artists)
 
     def callback_need_track_fb(self):
-        art = random.choice(self.player.list('artist'))
-        self.log.debug('Random art: {}'.format(art))
-        if self.mode == 'sensitive':
+        trks = list()
+        target = self.plugin_conf.getint('track_to_add')
+        limit = 0
+        while len(trks) < target:
+            trk = self.get_trk()
+            if trk:
+                trks.append(trk)
+            else:
+                limit += 1
+                if limit > 3:
+                    return trks
+        return trks
+
+    def get_trk(self):
+        """Get a single track acording to random flavour
+        """
+        trk = None
+        art = None
+        artists = list(self.player.artists)
+        if self.mode == 'sensible':
             played_art = self.get_played_artist()
-            while 42:
-                art = random.choice(self.player.list('artist'))
+            while artists:
+                art = random.choice(artists)
                 if art not in played_art:
                     break
-        trk  = random.choice(self.player.find_track(art))
-        self.log.info('random fallback ({}): {}'.format(self.mode, trk))
-        return [trk]
+                artists.pop(art)
+        elif self.mode == 'pure':
+            art = random.choice(artists)
+        if art is None:
+            return None
+        self.log.debug('Random art: {}'.format(art))
+        trks = self.player.find_track(art)
+        if trks:
+            trk = random.choice(trks)
+            self.log.info('random fallback ({}): {}'.format(self.mode, trk))
+        return trk
 
 
 
diff --git a/sima/utils/__init__.py b/sima/utils/__init__.py
index e69de29..abc7282 100644
--- a/sima/utils/__init__.py
+++ b/sima/utils/__init__.py
@@ -0,0 +1 @@
+#  pylint: disable=C0111
diff --git a/sima/utils/config.py b/sima/utils/config.py
index dbc29d6..cdaefd1 100644
--- a/sima/utils/config.py
+++ b/sima/utils/config.py
@@ -1,6 +1,5 @@
 # -*- coding: utf-8 -*-
-
-# Copyright (c) 2009, 2010, 2011, 2013 Jack Kaliko <kaliko at azylum.org>
+# Copyright (c) 2009, 2010, 2011, 2013, 2014 Jack Kaliko <kaliko at azylum.org>
 #
 #  This file is part of sima
 #
@@ -43,43 +42,47 @@ DEFAULT_CONF = {
         'MPD': {
             'host': "localhost",
             #'password': "",
-            'port': "6600",
+            'port': 6600,
             },
         'sima': {
             'internal': "Crop, Lastfm, RandomFallBack",
             'contrib': "",
             'user_db': "false",
-            'history_duration': "8",
-            'queue_length': "1",
+            'history_duration': 8,
+            'queue_length': 1,
+            'var_dir': 'empty',
             },
         'daemon':{
-            'daemon': "false",
+            'daemon': False,
             'pidfile': "",
             },
         'log': {
             'verbosity': "info",
             'logfile': "",
             },
+        'crop': {
+            'consume': 10,
+            },
         'echonest': {
             'queue_mode': "track", #TODO control values
-            'dynamic': "15",
+            'max_art': 15,
             'single_album': "false",
-            'track_to_add': "1",
-            'album_to_add': "1",
-            'depth': "1",
+            'track_to_add': 1,
+            'album_to_add': 1,
+            'depth': 1,
             },
         'lastfm': {
-            'dynamic': "10",
-            'similarity': "15",
             'queue_mode': "track", #TODO control values
+            'max_art': 10,
             'single_album': "false",
-            'track_to_add': "1",
-            'album_to_add': "1",
-            'depth': "1",
+            'track_to_add': 1,
+            'album_to_add': 1,
+            'depth': 1,
+            'cache': True,
             },
         'randomfallback': {
-            'flavour': "sensible", # in pure, sensible, genre
-            'track_to_add': "1",
+            'flavour': "sensible", # in pure, sensible
+            'track_to_add': 1,
             }
         }
 #
@@ -103,22 +106,19 @@ class ConfMan(object):  # CONFIG MANAGER CLASS
     """
 
     def __init__(self, logger, options=None):
+        self.log = logger
         # options settings priority:
-        # defauts < conf. file < command line
+        # defauts < env. var. < conf. file < command line
         self.conf_file = options.get('conf_file')
-        self.config = None
-        self.defaults = dict(DEFAULT_CONF)
+        self.config = configparser.ConfigParser(inline_comment_prefixes='#')
+        self.config.read_dict(DEFAULT_CONF)
+        # update DEFAULT_CONF with env. var.
+        self.use_envar()
         self.startopt = options
-        ## Sima sqlite DB
-        self.db_file = None
 
-        self.log = logger
         ## INIT CALLS
-        self.use_envar()
         self.init_config()
-        self.control_conf()
         self.supersedes_config_with_cmd_line_options()
-        self.config['sima']['db_file'] = self.db_file
 
     def get_pw(self):
         try:
@@ -159,49 +159,13 @@ class ConfMan(object):  # CONFIG MANAGER CLASS
         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
         if mpd_host:
             self.log.info('Env. variable MPD_HOST set to "%s"' % mpd_host)
-            self.defaults['MPD']['host'] = mpd_host
+            self.config['MPD'].update(host=mpd_host)
         if passwd:
             self.log.info('Env. variable MPD_HOST contains password.')
-            self.defaults['MPD']['password'] = passwd
+            self.config['MPD'].update(password=passwd)
         if mpd_port:
-            self.log.info('Env. variable MPD_PORT set to "%s".'
-                                  % mpd_port)
-            self.defaults['MPD']['port'] = mpd_port
-
-    def control_conf(self):
-        """Get through options/values and set defaults if not in conf file."""
-        # Control presence of obsolete settings
-        for option in ['history', 'history_length', 'top_tracks']:
-            if self.config.has_option('sima', option):
-                self.log.warning('Obsolete setting found in conf file: "%s"'
-                        % option)
-        # Setting default if not specified
-        for section in DEFAULT_CONF.keys():
-            if section not in self.config.sections():
-                self.log.debug('[%s] NOT in conf file' % section)
-                self.config.add_section(section)
-                for option in self.defaults[section]:
-                    self.config.set(section,
-                            option,
-                            self.defaults[section][option])
-                    self.log.debug(
-                            'Setting option with default value: %s = %s' %
-                            (option, self.defaults[section][option]))
-            elif section in self.config.sections():
-                self.log.debug('[%s] present in conf file' % section)
-                for option in self.defaults[section]:
-                    if self.config.has_option(section, option):
-                        #self.log.debug(u'option "%s" set to "%s" in conf. file' %
-                        #              (option, self.config.get(section, option)))
-                        pass
-                    else:
-                        self.log.debug(
-                                'Option "%s" missing in section "%s"' %
-                                (option, section))
-                        self.log.debug('=> setting default "%s" (may not suit you…)' %
-                                       self.defaults[section][option])
-                        self.config.set(section, option,
-                                        self.defaults[section][option])
+            self.log.info('Env. variable MPD_PORT set to "%s".' % mpd_port)
+            self.config['MPD'].update(port=mpd_port)
 
     def init_config(self):
         """
@@ -214,10 +178,7 @@ class ConfMan(object):  # CONFIG MANAGER CLASS
 
         if environ.get('XDG_DATA_HOME'):
             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
-        elif self.startopt.get('var_dir'):
-            # If var folder is provided via CLI set data_dir accordingly
-            data_dir = join(self.startopt.get('var_dir'))
-        elif (homedir and isdir(homedir) and homedir not in ['/']):
+        elif homedir and isdir(homedir) and homedir not in ['/']:
             data_dir = join(homedir, '.local', 'share', DIRNAME)
         else:
             self.log.error('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
@@ -233,7 +194,7 @@ class ConfMan(object):  # CONFIG MANAGER CLASS
             pass
         elif environ.get('XDG_CONFIG_HOME'):
             conf_dir = join(environ.get('XDG_CONFIG_HOME'), DIRNAME)
-        elif (homedir and isdir(homedir) and homedir not in ['/']):
+        elif homedir and isdir(homedir) and homedir not in ['/']:
             conf_dir = join(homedir, '.config', DIRNAME)
             # Create conf_dir if necessary
             if not isdir(conf_dir):
@@ -245,24 +206,22 @@ class ConfMan(object):  # CONFIG MANAGER CLASS
             self.log.error('Please use "--config" to locate the conf file')
             sys.exit(1)
 
-        self.db_file = join(data_dir, 'sima.db')
+        ## Sima sqlite DB
+        self.config['sima']['var_dir'] = join(data_dir)
+        self.config['sima']['db_file'] = join(data_dir, 'sima.db')
 
-        config = configparser.SafeConfigParser()
         # If no conf file present, uses defaults
         if not isfile(self.conf_file):
-            self.config = config
             return
 
         self.log.info('Loading configuration from:  %s' % self.conf_file)
         self.control_mod()
 
         try:
-            config.read(self.conf_file)
+            self.config.read(self.conf_file)
         except Error as err:
             self.log.error(err)
             sys.exit(1)
 
-        self.config = config
-
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/sima/utils/filelock.py b/sima/utils/filelock.py
new file mode 100644
index 0000000..8f7065f
--- /dev/null
+++ b/sima/utils/filelock.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009 Evan Fosmark
+# Copyright (c) 2014 Jack Kaliko <kaliko at azylum.org>
+#
+#   This program is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+# https://github.com/dmfrey/FileLock
+"""
+Plain file lock to une in context:
+    >>> with FileLock('/path/to/file/to/write'):
+    >>>     # a lock file is maintain within the scope of this context:
+    >>>     # /path/to/file/to/write.lock
+    >>>     ... # process file writing
+"""
+
+import errno
+import os
+import time
+
+class FileLockException(Exception):
+    """FileLock Exception"""
+    pass
+
+class FileLock:
+    """ A plain file lock whit context-manager"""
+
+    def __init__(self, file_name, timeout=10, delay=.05):
+        """
+        Setup file lock.
+        Setup timeout and the delay.
+        """
+        self.filedsc = None
+        self.is_locked = False
+        dirname = os.path.dirname(file_name)
+        self.lockfile = os.path.join(dirname, '{0}.lock'.format(file_name))
+        self.file_name = file_name
+        self.timeout = timeout
+        self.delay = delay
+
+    def acquire(self):
+        """Acquire the lock, if possible.
+        """
+        start_time = time.time()
+        while True:
+            try:
+                self.filedsc = os.open(self.lockfile,
+                                       os.O_CREAT|os.O_EXCL|os.O_RDWR)
+                break
+            except OSError as err:
+                if err.errno != errno.EEXIST:
+                    raise
+                if (time.time() - start_time) >= self.timeout:
+                    raise FileLockException('Timeout occured.')
+                time.sleep(self.delay)
+        self.is_locked = True
+
+    def release(self):
+        """Release the lock.
+        """
+        if self.is_locked:
+            os.close(self.filedsc)
+            os.unlink(self.lockfile)
+            self.is_locked = False
+
+    def __enter__(self):
+        """start of the with statement.
+        """
+        if not self.is_locked:
+            self.acquire()
+        return self
+
+    def __exit__(self, type, value, traceback):
+        """end of the with statement
+        """
+        if self.is_locked:
+            self.release()
+
+    def __del__(self):
+        """Cleanup
+        """
+        self.release()
diff --git a/sima/utils/leven.py b/sima/utils/leven.py
index adc48f7..ab6fc35 100644
--- a/sima/utils/leven.py
+++ b/sima/utils/leven.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-
 # Copyright (c) 2009, 2010, 2013 Jack Kaliko <kaliko at azylum.org>
 #
 #  This file is part of sima
@@ -17,7 +16,7 @@
 #  You should have received a copy of the GNU General Public License
 #  along with sima.  If not, see <http://www.gnu.org/licenses/>.
 #
-#
+"""Computes levenshtein distance/ratio"""
 
 def levenshtein(a_st, b_st):
     """Computes the Levenshtein distance between two strings."""
diff --git a/sima/utils/startopt.py b/sima/utils/startopt.py
index 04df3b4..ef2032c 100644
--- a/sima/utils/startopt.py
+++ b/sima/utils/startopt.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2009, 2010, 2011, 2012, 2013 Jack Kaliko <kaliko at azylum.org>
+# Copyright (c) 2009, 2010, 2011, 2012, 2013, 2014 Jack Kaliko <kaliko at azylum.org>
 #
 #  This file is part of sima
 #
@@ -19,11 +19,10 @@
 #
 #
 
-import sys
 from argparse import (ArgumentParser, SUPPRESS)
 
 
-from .utils import Obsolete, Wfile, Rfile, Wdir
+from .utils import Wfile, Rfile, Wdir
 
 USAGE = """USAGE:  %prog [--help] [options]"""
 DESCRIPTION = """
@@ -115,6 +114,7 @@ class StartOpt(object):
     """
 
     def __init__(self, script_info,):
+        self.parser = None
         self.info = dict(script_info)
         self.options = dict()
         self.main()
diff --git a/sima/utils/utils.py b/sima/utils/utils.py
index 93bac25..ff06410 100644
--- a/sima/utils/utils.py
+++ b/sima/utils/utils.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (c) 2010, 2011, 2013 Jack Kaliko <kaliko at azylum.org>
+# Copyright (c) 2010, 2011, 2013, 2014 Jack Kaliko <kaliko at azylum.org>
 #
 #  This file is part of sima
 #
@@ -20,6 +20,7 @@
 #
 """generic tools and utilities for sima
 """
+# pylint: disable=C0111
 
 import traceback
 import sys
@@ -27,7 +28,7 @@ import sys
 from argparse import ArgumentError, Action
 from base64 import b64decode as push
 from codecs import getencoder
-from datetime import datetime, timedelta
+from datetime import datetime
 from os import environ, access, getcwd, W_OK, R_OK
 from os.path import dirname, isabs, join, normpath, exists, isdir, isfile
 from time import sleep
@@ -78,20 +79,9 @@ def exception_log():
     log.info('Quiting now!')
     sys.exit(1)
 
-def purge_cache(obj, age=4):
-    now = datetime.utcnow()
-    if now.hour == obj.timestamp.hour:
-        return
-    obj.timestamp = datetime.utcnow()
-    cache = obj.cache
-    delta = timedelta(hours=age)
-    for url in list(cache.keys()):
-        timestamp = cache.get(url).created()
-        if now - timestamp > delta:
-            cache.pop(url)
-
 
 class SigHup(Exception):
+    """SIGHUP raises this Exception"""
     pass
 
 # ArgParse Callbacks
@@ -158,7 +148,8 @@ class Wdir(FileAction):
         if not access(self._file, W_OK):
             self.parser.error('no write access to "{0}"'.format(self._file))
 
-class Throttle():
+class Throttle:
+    """throttle decorator"""
     def __init__(self, wait):
         self.wait = wait
         self.last_called = datetime.now()
@@ -172,18 +163,20 @@ class Throttle():
             return result
         return wrapper
 
-class Cache():
-    def __init__(self, elem, last=None):
-        self.elem = elem
-        self.requestdate = last
-        if not last:
-            self.requestdate = datetime.utcnow()
+# http client exceptions (for webservices)
+
+class WSError(Exception):
+    pass
+
+class WSNotFound(WSError):
+    pass
+
+class WSTimeout(WSError):
+    pass
 
-    def created(self):
-        return self.requestdate
+class WSHTTPError(WSError):
+    pass
 
-    def get(self):
-        return self.elem
 
 # VIM MODLINE
 # vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/simadb_cli b/simadb_cli
new file mode 100755
index 0000000..52f45bc
--- /dev/null
+++ b/simadb_cli
@@ -0,0 +1,566 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2010-2013 Jack Kaliko <efrim at azylum.org>
+#
+#  This file is part of MPD_sima
+#
+#  MPD_sima is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  MPD_sima is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with MPD_sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+__version__ = '0.4.0'
+
+# IMPORT#
+import re
+
+from argparse import (ArgumentParser, SUPPRESS, Action)
+from difflib import get_close_matches
+from locale import getpreferredencoding
+from os import (environ, chmod, makedirs)
+from os.path import (join, isdir, isfile, expanduser)
+from sys import (exit, stdout, stderr)
+
+from sima.lib.track import Track
+from sima.utils import utils
+from sima.lib import simadb
+from musicpd import MPDClient, ConnectionError
+
+
+DESCRIPTION = """
+simadb_cli helps you to edit entries in your own DB of similarity
+between artists."""
+DB_NAME = 'sima.db'
+
+class FooAction(Action):
+    def check(self, namespace):
+        if namespace.similarity: return True
+        if namespace.remove_art: return True
+        if namespace.remove_sim: return True
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        opt_required = '"--remove_artist", "--remove_similarity" or "--add_similarity"'
+        if not self.check(namespace):
+            parser.error(
+                    'can\'t use {0} option before or without {1}'.format(
+                        option_string, opt_required))
+        setattr(namespace, self.dest, True)
+
+# Options list
+# pop out 'sw' value before creating ArgumentParser object.
+OPTS = list([
+    {
+        'sw':['-a', '--add_similarity'],
+        'type': str,
+        'dest':'similarity',
+        'help': 'Similarity to add formated as follow: ' +
+        ' "art_0,art_1:90,art_2:80..."'},
+    {
+        'sw': ['-c', '--check_names'],
+        'action': 'store_true',
+        'default': False,
+        'help': 'Turn on controls of artists names in MPD library.'},
+    {
+        'sw':['-d', '--dbfile'],
+        'type': str,
+        'dest':'dbfile',
+        'action': utils.Wfile,
+        'help': 'File to read/write database from/to'},
+    {
+        'sw': ['-r', '--reciprocal'],
+        'default': False,
+        'nargs': 0, 
+        'action': FooAction,
+        'help': 'Turn on reciprocity for similarity relation when add/remove.'},
+    {
+        'sw':['--remove_artist'],
+        'type': str,
+        'dest': 'remove_art',
+        'metavar': '"ARTIST TO REMOVE"',
+        'help': 'Remove an artist from DB (main artist entries).'},
+    {
+        'sw':['--remove_similarity'],
+        'type': str,
+        'dest': 'remove_sim',
+        'metavar': '"MAIN ART,SIMI ART"',
+        'help': 'Remove an similarity relation from DB (main artist <=> similar artist).'},
+    {
+        'sw':['-v', '--view_artist'],
+        'type': str,
+        'dest':'view',
+        'metavar': '"ARTIST NAME"',
+        'help': 'View an artist from DB.'},
+    {
+        'sw':['--view_all'],
+        'action': 'store_true',
+        'help': 'View all similarity entries.'},
+    {
+        'sw': ['-S', '--host'],
+        'type': str,
+        'dest': 'mpdhost',
+        'default': None,
+        'help': 'MPD host, as IP or FQDN (default: localhost|MPD_HOST).'},
+    {
+        'sw': ['-P', '--port'],
+        'type': int,
+        'dest': 'mpdport',
+        'default': None,
+        'help': 'Port MPD in listening on (default: 6600|MPD_PORT).'},
+    {
+        'sw': ['--password'],
+        'type': str,
+        'dest': 'passwd',
+        'default': None,
+        'help': SUPPRESS},
+    {
+        'sw': ['--view_bl'],
+        'action': 'store_true',
+        'help': 'View black list.'},
+    {
+        'sw': ['--remove_bl'],
+        'type': int,
+        'help': 'Suppress a black list entry, by row id. Use --view_bl to get row id.'},
+    {
+        'sw': ['--bl_art'],
+        'type': str,
+        'metavar': 'ARTIST_NAME',
+        'help': 'Black list artist.'},
+    {
+        'sw': ['--bl_curr_art'],
+        'action': 'store_true',
+        'help': 'Black list currently playing artist.'},
+    {
+        'sw': ['--bl_curr_alb'],
+        'action': 'store_true',
+        'help': 'Black list currently playing album.'},
+    {
+        'sw': ['--bl_curr_trk'],
+        'action': 'store_true',
+        'help': 'Black list currently playing track.'},
+    {
+        'sw':['--purge_hist'],
+        'action': 'store_true',
+        'dest': 'do_purge_hist',
+        'help': 'Purge play history.'}])
+
+
+class SimaDB_CLI(object):
+    """Command line management.
+    """
+
+    def __init__(self):
+        self.dbfile = self._get_default_dbfile()
+        self.parser = None
+        self.options = dict({})
+        self.localencoding = 'UTF-8'
+        self._get_encoding()
+        self._upgrade()
+        self.main()
+
+    def _get_encoding(self):
+        """Get local encoding"""
+        localencoding = getpreferredencoding()
+        if localencoding:
+            self.localencoding = localencoding
+
+    def _get_mpd_env_var(self):
+        """
+        MPD host/port environement variables are used if command line does not
+        provide host|port|passwd.
+        """
+        host, port, passwd = utils.get_mpd_environ()
+        if self.options.passwd is None and passwd:
+            self.options.passwd = passwd
+        if self.options.mpdhost is None:
+            if host:
+                self.options.mpdhost = host
+            else:
+                self.options.mpdhost = 'localhost'
+        if self.options.mpdport is None:
+            if port:
+                self.options.mpdport = port
+            else:
+                self.options.mpdport = 6600
+
+    def _upgrade(self):
+        """Upgrades DB if necessary, create one if not existing."""
+        if not isfile(self.dbfile): # No db file
+            return
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db.upgrade()
+
+    def _declare_opts(self):
+        """
+        Declare options in ArgumentParser object.
+        """
+        self.parser = ArgumentParser(description=DESCRIPTION,
+                                   usage='%(prog)s [-h|--help] [options]',
+                                   prog='simadb_cli',
+                                   epilog='Happy Listening',
+                                   )
+
+        self.parser.add_argument('--version', action='version',
+                version='%(prog)s {0}'.format(__version__))
+        # Add all options declare in OPTS
+        for opt in OPTS:
+            opt_names = opt.pop('sw')
+            self.parser.add_argument(*opt_names, **opt)
+
+    def _get_default_dbfile(self):
+        """
+        Use XDG directory standard if exists
+        else use "HOME/.local/share/mpd_sima/"
+        http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
+        """
+        homedir = expanduser('~')
+        dirname = 'mpd_sima'
+        if environ.get('XDG_DATA_HOME'):
+            data_dir = join(environ.get('XDG_DATA_HOME'), dirname)
+        else:
+            data_dir = join(homedir, '.local', 'share', dirname)
+        if not isdir(data_dir):
+            makedirs(data_dir)
+            chmod(data_dir, 0o700)
+        return join(data_dir, DB_NAME)
+
+    def _get_mpd_client(self):
+        """"""
+        # TODO: encode properly host name
+        host = self.options.mpdhost
+        port = self.options.mpdport
+        cli = MPDClient()
+        try:
+            cli.connect(host=host, port=port)
+        except ConnectionError as err:
+            mess = 'ERROR: fail to connect MPD (host: %s:%s): %s' % (
+                    host, port, err)
+            print(mess, file=stderr)
+            exit(1)
+        return cli
+
+    def _create_db(self):
+        """Create database if necessary"""
+        if isfile(self.dbfile):
+            return
+        print('Creating database!')
+        open(self.dbfile, 'a').close()
+        simadb.SimaDB(db_path=self.dbfile).create_db()
+
+    def _get_art_from_db(self, art):
+        """Return (id, name, self...) from DB or None is not in DB"""
+        db = simadb.SimaDB(db_path=self.dbfile)
+        art_db = db.get_artist(art, add_not=True)
+        if not art_db:
+            print('ERROR: "%s" not in data base!' % art, file=stderr)
+            return None
+        return art_db
+
+    def _control_similarity(self):
+        """
+         * Regex check of command line similarity
+         * Controls artist presence in MPD library
+        """
+        usage = ('USAGE: "main artist,similar artist:<match score>,other' +
+                'similar artist:<match score>,..."')
+        cli_sim = self.options.similarity
+        pattern = '^([^,]+?),([^:,]+?:\d{1,2},?)+$'
+        regexp = re.compile(pattern, re.U).match(cli_sim)
+        if not regexp:
+            mess = 'ERROR: similarity badly formated: "%s"' % cli_sim
+            print(mess, file=stderr)
+            print(usage, file=stderr)
+            exit(1)
+        if self.options.check_names:
+            if not self._control_artist_names():
+                mess = 'ERROR: some artist names not found in MPD library!'
+                print(mess, file=stderr)
+                exit(1)
+
+    def _control_artist_names(self):
+        """Controls artist names exist in MPD library"""
+        mpd_cli = self._get_mpd_client()
+        artists_list = mpd_cli.list('artist')
+        sim_formated = self._parse_similarity()
+        control = True
+        if sim_formated[0] not in artists_list:
+            mess = 'WARNING: Main artist not found in MPD: %s' % sim_formated[0]
+            print(mess)
+            control = False
+        for sart in sim_formated[1]:
+            art = sart.get('artist')
+            if art not in artists_list:
+                mess = str('WARNING: Similar artist not found in MPD: %s' % art)
+                print(mess)
+                control = False
+        mpd_cli.disconnect()
+        return control
+
+    def _parse_similarity(self):
+        """Parse command line option similarity"""
+        cli_sim = self.options.similarity.strip(',').split(',')
+        sim = list([])
+        main = cli_sim[0]
+        for art in cli_sim[1:]:
+            artist = art.split(':')[0]
+            score = int(art.split(':')[1])
+            sim.append({'artist': artist, 'score': score})
+        return (main, sim)
+
+    def _print_main_art(self, art=None):
+        """Print entries, art as main artist."""
+        if not art:
+            art = self.options.view
+        db = simadb.SimaDB(db_path=self.dbfile)
+        art_db = self._get_art_from_db(art)
+        if not art_db: return
+        sims = list([])
+        [sims.append(a) for a in db._get_similar_artists_from_db(art_db[0])]
+        if len(sims) == 0:
+            return False
+        print('"%s" similarities:' % art)
+        for art in sims:
+            mess = str('  - {score:0>2d} {artist}'.format(**art))
+            print(mess)
+        return True
+
+    def _remove_sim(self, art1_db, art2_db):
+        """Remove single similarity between two artists."""
+        db = simadb.SimaDB(db_path=self.dbfile)
+        similarity = db._get_artist_match(art1_db[0], art2_db[0])
+        if similarity == 0:
+            return False
+        db._remove_relation_between_2_artist(art1_db[0], art2_db[0])
+        mess = 'Remove: "{0}" "{1}:{2:0>2d}"'.format(art1_db[1], art2_db[1],
+                                                     similarity)
+        print(mess)
+        return True
+
+    def _revert_similarity(self, sim_formated):
+        """Revert similarity string (for reciprocal editing - add)."""
+        main_art = sim_formated[0]
+        similars = sim_formated[1]
+        for similar in similars:
+            yield (similar.get('artist'),
+                [{'artist':main_art, 'score':similar.get('score')}])
+
+    def bl_artist(self):
+        """Black list artist"""
+        mpd_cli = self._get_mpd_client()
+        if not mpd_cli:
+            return False
+        artists_list = mpd_cli.list('artist')
+        # Unicode cli given artist name
+        cli_artist_to_bl = self.options.bl_art
+        if cli_artist_to_bl not in artists_list:
+            print('Artist not found in MPD library.')
+            match = get_close_matches(cli_artist_to_bl, artists_list, 50, 0.78)
+            if match:
+                print('You may be refering to %s' %
+                        '/'.join([m_a for m_a in match]))
+            return False
+        print('Black listing artist: %s' % cli_artist_to_bl)
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db.get_bl_artist(cli_artist_to_bl)
+
+    def bl_current_artist(self):
+        """Black list current artist"""
+        mpd_cli = self._get_mpd_client()
+        if not mpd_cli:
+            return False
+        artist = mpd_cli.currentsong().get('artist', '')
+        if not artist:
+            print('No artist found.')
+            return False
+        print('Black listing artist: %s' % artist)
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db.get_bl_artist(artist)
+
+    def bl_current_album(self):
+        """Black list current artist"""
+        mpd_cli = self._get_mpd_client()
+        if not mpd_cli:
+            return False
+        track = Track(**mpd_cli.currentsong())
+        if not track.album:
+            print('No album set for this track: %s' % track)
+            return False
+        print('Black listing album: {0}'.format(track.album))
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db.get_bl_album(track)
+
+    def bl_current_track(self):
+        """Black list current artist"""
+        mpd_cli = self._get_mpd_client()
+        if not mpd_cli:
+            return False
+        track = Track(**mpd_cli.currentsong())
+        print('Black listing track: %s' % track)
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db.get_bl_track(track)
+
+    def purge_history(self):
+        """Purge all entries in history"""
+        db = simadb.SimaDB(db_path=self.dbfile)
+        print('Purging history...')
+        db.purge_history(duration=int(0))
+        print('done.')
+        print('Cleaning database...')
+        db.clean_database()
+        print('done.')
+
+    def view(self):
+        """Print out entries for an artist."""
+        art = self.options.view
+        db = simadb.SimaDB(db_path=self.dbfile)
+        art_db = self._get_art_from_db(art)
+        if not art_db: return
+        if not self._print_main_art():
+            mess = str('"%s" present in DB but not as a main artist' % art)
+            print(mess)
+        else: print('')
+        art_rev = list([])
+        [art_rev.append(a) for a in db._get_reverse_similar_artists_from_db(art_db[0])]
+        if not art_rev: return
+        mess = str('%s" appears as similar for the following artist(s): %s' %
+                (art,', '.join(art_rev)))
+        print(mess)
+        [self._print_main_art(a) for a in art_rev]
+
+    def view_all(self):
+        """Print out all entries."""
+        db = simadb.SimaDB(db_path=self.dbfile)
+        for art in db.get_artists():
+            if not art[0]: continue
+            self._print_main_art(art=art[0])
+
+    def view_bl(self):
+        """Print out black list."""
+        # TODO: enhance output formating
+        db = simadb.SimaDB(db_path=self.dbfile)
+        for bl_e in db.get_black_list():
+            print('\t# '.join([str(e) for e in bl_e]))
+
+    def remove_similarity(self):
+        """Remove entry"""
+        cli_sim = self.options.remove_sim
+        pattern = '^([^,]+?),([^,]+?,?)$'
+        regexp = re.compile(pattern, re.U).match(cli_sim)
+        if not regexp:
+            print('ERROR: similarity badly formated: "%s"' % cli_sim, file=stderr)
+            print('USAGE: A single relation between two artists is expected here.', file=stderr)
+            print('USAGE: "main artist,similar artist"', file=stderr)
+            exit(1)
+        arts = cli_sim.split(',')
+        if len(arts) != 2:
+            print('ERROR: unknown error in similarity format', file=stderr)
+            print('USAGE: "main artist,similar artist"', file=stderr)
+            exit(1)
+        art1_db = self._get_art_from_db(arts[0].strip())
+        art2_db = self._get_art_from_db(arts[1].strip())
+        if not art1_db or not art2_db: return
+        self._remove_sim(art1_db, art2_db)
+        if not self.options.reciprocal:
+            return
+        self._remove_sim(art2_db, art1_db)
+
+    def remove_artist(self):
+        """ Remove artist in the DB."""
+        deep = False
+        art = self.options.remove_art
+        db = simadb.SimaDB(db_path=self.dbfile)
+        art_db = self._get_art_from_db(art)
+        if not art_db: return False
+        print('Removing "%s" from database' % art)
+        if self.options.reciprocal:
+            print('reciprocal option used, performing deep remove!')
+            deep = True
+        db._remove_artist(art_db[0], deep=deep)
+
+    def remove_black_list_entry(self):
+        """"""
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db._remove_bl(int(self.options.remove_bl))
+
+    def write_simi(self):
+        """Write similarity to DB.
+        """
+        self._create_db()
+        sim_formated = self._parse_similarity()
+        print('About to update DB with: "%s": %s' % sim_formated)
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db._update_similar_artists(*sim_formated)
+        if self.options.reciprocal:
+            print('...and with reciprocal combinations as well.')
+            for sim_formed_rec in self._revert_similarity(sim_formated):
+                db._update_similar_artists(*sim_formed_rec)
+
+    def main(self):
+        """
+        Parse command line and run actions.
+        """
+        self._declare_opts()
+        self.options = self.parser.parse_args()
+        self._get_mpd_env_var()
+        if self.options.dbfile:
+            self.dbfile = self.options.dbfile
+            print('Using db file: %s' % self.dbfile)
+        if self.options.reciprocal:
+            print('Editing reciprocal similarity')
+        if self.options.bl_art:
+            self.bl_artist()
+            return
+        if self.options.bl_curr_art:
+            self.bl_current_artist()
+            return
+        if self.options.bl_curr_alb:
+            self.bl_current_album()
+            return
+        if self.options.bl_curr_trk:
+            self.bl_current_track()
+            return
+        if self.options.view_bl:
+            self.view_bl()
+            return
+        if self.options.remove_bl:
+            self.remove_black_list_entry()
+            return
+        if self.options.similarity:
+            self._control_similarity()
+            self.write_simi()
+            return
+        if self.options.remove_art:
+            self.remove_artist()
+            return
+        if self.options.remove_sim:
+            self.remove_similarity()
+            return
+        if self.options.view:
+            self.view()
+            return
+        if self.options.view_all:
+            self.view_all()
+        if self.options.do_purge_hist:
+            self.purge_history()
+        exit(0)
+
+
+def main():
+    SimaDB_CLI()
+
+# Script starts here
+if __name__ == '__main__':
+    main()
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/vinstall.sh b/vinstall.sh
index b90dbbb..88d792a 100755
--- a/vinstall.sh
+++ b/vinstall.sh
@@ -35,7 +35,7 @@ virtualenv $VENV_OPTIONS $INSTALL_DIR/venv || { echo "something went wrong gener
 PIP_OPTIONS=""
 [ "$DEBUG" = "0" ] && PIP_OPTIONS="$PIP_OPTIONS --quiet"
 
-pip $PIP_OPTIONS install --pre python-musicpd || exit 1
+pip install -e . || exit 1
 
 echo Installing mpd-sima
 $(dirname $0)/setup.py --quiet install || exit 1
@@ -48,12 +48,12 @@ SIMA_VLAUNCHER=$INSTALL_DIR/vmpd-sima
 cat << EOF > "$SIMA_VLAUNCHER"
 #!/bin/sh
 . $INSTALL_DIR/venv/bin/activate
-$SIMA_LAUNCHER "\$@"
+$INSTALL_DIR/$SIMA_LAUNCHER "\$@"
 EOF
 chmod +x $SIMA_VLAUNCHER
 
 echo Cleaning up
-rm -rf $(dirname $0)/dist
+rm -rf $(dirnddame $0)/dist
 rm -rf $(dirname $0)/build
 rm -rf $(dirname $0)/sima.egg-info
 

-- 
mpd-sima packaging



More information about the pkg-multimedia-commits mailing list