]> git.parisson.com Git - deefuzzer.git/commitdiff
nouveau projet démarré
authorGuillaume Pellerin <yomguy@parisson.com>
Thu, 9 Sep 2010 13:52:08 +0000 (13:52 +0000)
committerGuillaume Pellerin <yomguy@parisson.com>
Thu, 9 Sep 2010 13:52:08 +0000 (13:52 +0000)
61 files changed:
trunk/COPYING [new file with mode: 0644]
trunk/INSTALL [new file with mode: 0644]
trunk/README [new file with mode: 0644]
trunk/__init__.py [new file with mode: 0644]
trunk/debian/README.Debian [new file with mode: 0644]
trunk/debian/changelog [new file with mode: 0644]
trunk/debian/compat [new file with mode: 0644]
trunk/debian/control [new file with mode: 0644]
trunk/debian/copyright [new file with mode: 0644]
trunk/debian/dirs [new file with mode: 0644]
trunk/debian/docs [new file with mode: 0644]
trunk/debian/manpage.1.ex [new file with mode: 0644]
trunk/debian/manpage.sgml.ex [new file with mode: 0644]
trunk/debian/manpage.xml.ex [new file with mode: 0644]
trunk/debian/rules [new file with mode: 0755]
trunk/deefuzzer.e4p [new file with mode: 0644]
trunk/deefuzzer.py [new file with mode: 0755]
trunk/example/deefuzz_test.xml [new file with mode: 0644]
trunk/example/deefuzz_xml_gen.py [new file with mode: 0755]
trunk/example/myfuzz.xml [new file with mode: 0644]
trunk/example/test.xml [new file with mode: 0644]
trunk/example/yomguy_test.xml [new file with mode: 0644]
trunk/example/yomguy_test1.xml [new file with mode: 0644]
trunk/install.py [new file with mode: 0644]
trunk/kill_deefuzzer.sh [new file with mode: 0755]
trunk/shout-python/COPYING [new file with mode: 0644]
trunk/shout-python/INSTALL [new file with mode: 0644]
trunk/shout-python/MANIFEST [new file with mode: 0644]
trunk/shout-python/README [new file with mode: 0644]
trunk/shout-python/example.py [new file with mode: 0755]
trunk/shout-python/setup.py [new file with mode: 0644]
trunk/shout-python/shout.c [new file with mode: 0644]
trunk/tests/test_twitter2.py [new file with mode: 0644]
trunk/tools/PyRSS2Gen.py [new file with mode: 0644]
trunk/tools/__init__.py [new file with mode: 0644]
trunk/tools/get_access_token.py [new file with mode: 0644]
trunk/tools/logger.py [new file with mode: 0644]
trunk/tools/mp3.py [new file with mode: 0644]
trunk/tools/ogg.py [new file with mode: 0644]
trunk/tools/osc.py [new file with mode: 0644]
trunk/tools/osc_jingles_start.py [new file with mode: 0644]
trunk/tools/osc_jingles_stop.py [new file with mode: 0644]
trunk/tools/osc_player_fast.py [new file with mode: 0644]
trunk/tools/osc_player_next.py [new file with mode: 0644]
trunk/tools/osc_player_next2.py [new file with mode: 0644]
trunk/tools/osc_player_slow.py [new file with mode: 0644]
trunk/tools/osc_record_start.py [new file with mode: 0644]
trunk/tools/osc_record_stop.py [new file with mode: 0644]
trunk/tools/osc_relay_start.py [new file with mode: 0644]
trunk/tools/osc_relay_stop.py [new file with mode: 0644]
trunk/tools/osc_run_stop.py [new file with mode: 0644]
trunk/tools/osc_twitter_start.py [new file with mode: 0644]
trunk/tools/osc_twitter_stop.py [new file with mode: 0644]
trunk/tools/player.py [new file with mode: 0644]
trunk/tools/recorder.py [new file with mode: 0644]
trunk/tools/relay.py [new file with mode: 0644]
trunk/tools/station.py [new file with mode: 0644]
trunk/tools/tools.py [new file with mode: 0644]
trunk/tools/twitt.py [new file with mode: 0644]
trunk/tools/xmltodict.py [new file with mode: 0644]
trunk/tools/xmltodict2.py [new file with mode: 0644]

diff --git a/trunk/COPYING b/trunk/COPYING
new file mode 100644 (file)
index 0000000..ad5f994
--- /dev/null
@@ -0,0 +1,505 @@
+CeCILL FREE SOFTWARE LICENSE AGREEMENT
+
+
+    Notice
+
+This Agreement is a Free Software license agreement that is the result
+of discussions between its authors in order to ensure compliance with
+the two main principles guiding its drafting:
+
+    * firstly, compliance with the principles governing the distribution
+      of Free Software: access to source code, broad rights granted to
+      users,
+    * secondly, the election of a governing law, French law, with which
+      it is conformant, both as regards the law of torts and
+      intellectual property law, and the protection that it offers to
+      both authors and holders of the economic rights over software.
+
+The authors of the CeCILL (for Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre])
+license are:
+
+Commissariat à l'Energie Atomique - CEA, a public scientific, technical
+and industrial research establishment, having its principal place of
+business at 25 rue Leblanc, immeuble Le Ponant D, 75015 Paris, France.
+
+Centre National de la Recherche Scientifique - CNRS, a public scientific
+and technological establishment, having its principal place of business
+at 3 rue Michel-Ange, 75794 Paris cedex 16, France.
+
+Institut National de Recherche en Informatique et en Automatique -
+INRIA, a public scientific and technological establishment, having its
+principal place of business at Domaine de Voluceau, Rocquencourt, BP
+105, 78153 Le Chesnay cedex, France.
+
+
+    Preamble
+
+The purpose of this Free Software license agreement is to grant users
+the right to modify and redistribute the software governed by this
+license within the framework of an open source distribution model.
+
+The exercising of these rights is conditional upon certain obligations
+for users so as to preserve this status for all subsequent redistributions.
+
+In consideration of access to the source code and the rights to copy,
+modify and redistribute granted by the license, users are provided only
+with a limited warranty and the software's author, the holder of the
+economic rights, and the successive licensors only have limited liability.
+
+In this respect, the risks associated with loading, using, modifying
+and/or developing or reproducing the software by the user are brought to
+the user's attention, given its Free Software status, which may make it
+complicated to use, with the result that its use is reserved for
+developers and experienced professionals having in-depth computer
+knowledge. Users are therefore encouraged to load and test the
+suitability of the software as regards their requirements in conditions
+enabling the security of their systems and/or data to be ensured and,
+more generally, to use and operate it in the same conditions of
+security. This Agreement may be freely reproduced and published,
+provided it is not altered, and that no provisions are either added or
+removed herefrom.
+
+This Agreement may apply to any or all software for which the holder of
+the economic rights decides to submit the use thereof to its provisions.
+
+
+    Article 1 - DEFINITIONS
+
+For the purpose of this Agreement, when the following expressions
+commence with a capital letter, they shall have the following meaning:
+
+Agreement: means this license agreement, and its possible subsequent
+versions and annexes.
+
+Software: means the software in its Object Code and/or Source Code form
+and, where applicable, its documentation, "as is" when the Licensee
+accepts the Agreement.
+
+Initial Software: means the Software in its Source Code and possibly its
+Object Code form and, where applicable, its documentation, "as is" when
+it is first distributed under the terms and conditions of the Agreement.
+
+Modified Software: means the Software modified by at least one
+Contribution.
+
+Source Code: means all the Software's instructions and program lines to
+which access is required so as to modify the Software.
+
+Object Code: means the binary files originating from the compilation of
+the Source Code.
+
+Holder: means the holder(s) of the economic rights over the Initial
+Software.
+
+Licensee: means the Software user(s) having accepted the Agreement.
+
+Contributor: means a Licensee having made at least one Contribution.
+
+Licensor: means the Holder, or any other individual or legal entity, who
+distributes the Software under the Agreement.
+
+Contribution: means any or all modifications, corrections, translations,
+adaptations and/or new functions integrated into the Software by any or
+all Contributors, as well as any or all Internal Modules.
+
+Module: means a set of sources files including their documentation that
+enables supplementary functions or services in addition to those offered
+by the Software.
+
+External Module: means any or all Modules, not derived from the
+Software, so that this Module and the Software run in separate address
+spaces, with one calling the other when they are run.
+
+Internal Module: means any or all Module, connected to the Software so
+that they both execute in the same address space.
+
+GNU GPL: means the GNU General Public License version 2 or any
+subsequent version, as published by the Free Software Foundation Inc.
+
+Parties: mean both the Licensee and the Licensor.
+
+These expressions may be used both in singular and plural form.
+
+
+    Article 2 - PURPOSE
+
+The purpose of the Agreement is the grant by the Licensor to the
+Licensee of a non-exclusive, transferable and worldwide license for the
+Software as set forth in Article 5 hereinafter for the whole term of the
+protection granted by the rights over said Software. 
+
+
+    Article 3 - ACCEPTANCE
+
+3.1 The Licensee shall be deemed as having accepted the terms and
+conditions of this Agreement upon the occurrence of the first of the
+following events:
+
+    * (i) loading the Software by any or all means, notably, by
+      downloading from a remote server, or by loading from a physical
+      medium;
+    * (ii) the first time the Licensee exercises any of the rights
+      granted hereunder.
+
+3.2 One copy of the Agreement, containing a notice relating to the
+characteristics of the Software, to the limited warranty, and to the
+fact that its use is restricted to experienced users has been provided
+to the Licensee prior to its acceptance as set forth in Article 3.1
+hereinabove, and the Licensee hereby acknowledges that it has read and
+understood it.
+
+
+    Article 4 - EFFECTIVE DATE AND TERM
+
+
+      4.1 EFFECTIVE DATE
+
+The Agreement shall become effective on the date when it is accepted by
+the Licensee as set forth in Article 3.1.
+
+
+      4.2 TERM
+
+The Agreement shall remain in force for the entire legal term of
+protection of the economic rights over the Software.
+
+
+    Article 5 - SCOPE OF RIGHTS GRANTED
+
+The Licensor hereby grants to the Licensee, who accepts, the following
+rights over the Software for any or all use, and for the term of the
+Agreement, on the basis of the terms and conditions set forth hereinafter.
+
+Besides, if the Licensor owns or comes to own one or more patents
+protecting all or part of the functions of the Software or of its
+components, the Licensor undertakes not to enforce the rights granted by
+these patents against successive Licensees using, exploiting or
+modifying the Software. If these patents are transferred, the Licensor
+undertakes to have the transferees subscribe to the obligations set
+forth in this paragraph.
+
+
+      5.1 RIGHT OF USE
+
+The Licensee is authorized to use the Software, without any limitation
+as to its fields of application, with it being hereinafter specified
+that this comprises:
+
+   1. permanent or temporary reproduction of all or part of the Software
+      by any or all means and in any or all form.
+
+   2. loading, displaying, running, or storing the Software on any or
+      all medium.
+
+   3. entitlement to observe, study or test its operation so as to
+      determine the ideas and principles behind any or all constituent
+      elements of said Software. This shall apply when the Licensee
+      carries out any or all loading, displaying, running, transmission
+      or storage operation as regards the Software, that it is entitled
+      to carry out hereunder.
+
+
+      5.2 ENTITLEMENT TO MAKE CONTRIBUTIONS
+
+The right to make Contributions includes the right to translate, adapt,
+arrange, or make any or all modifications to the Software, and the right
+to reproduce the resulting software.
+
+The Licensee is authorized to make any or all Contributions to the
+Software provided that it includes an explicit notice that it is the
+author of said Contribution and indicates the date of the creation thereof.
+
+
+      5.3 RIGHT OF DISTRIBUTION
+
+In particular, the right of distribution includes the right to publish,
+transmit and communicate the Software to the general public on any or
+all medium, and by any or all means, and the right to market, either in
+consideration of a fee, or free of charge, one or more copies of the
+Software by any means.
+
+The Licensee is further authorized to distribute copies of the modified
+or unmodified Software to third parties according to the terms and
+conditions set forth hereinafter.
+
+
+        5.3.1 DISTRIBUTION OF SOFTWARE WITHOUT MODIFICATION
+
+The Licensee is authorized to distribute true copies of the Software in
+Source Code or Object Code form, provided that said distribution
+complies with all the provisions of the Agreement and is accompanied by:
+
+   1. a copy of the Agreement,
+
+   2. a notice relating to the limitation of both the Licensor's
+      warranty and liability as set forth in Articles 8 and 9,
+
+and that, in the event that only the Object Code of the Software is
+redistributed, the Licensee allows future Licensees unhindered access to
+the full Source Code of the Software by indicating how to access it, it
+being understood that the additional cost of acquiring the Source Code
+shall not exceed the cost of transferring the data.
+
+
+        5.3.2 DISTRIBUTION OF MODIFIED SOFTWARE
+
+When the Licensee makes a Contribution to the Software, the terms and
+conditions for the distribution of the resulting Modified Software
+become subject to all the provisions of this Agreement.
+
+The Licensee is authorized to distribute the Modified Software, in
+source code or object code form, provided that said distribution
+complies with all the provisions of the Agreement and is accompanied by:
+
+   1. a copy of the Agreement,
+
+   2. a notice relating to the limitation of both the Licensor's
+      warranty and liability as set forth in Articles 8 and 9,
+
+and that, in the event that only the object code of the Modified
+Software is redistributed, the Licensee allows future Licensees
+unhindered access to the full source code of the Modified Software by
+indicating how to access it, it being understood that the additional
+cost of acquiring the source code shall not exceed the cost of
+transferring the data.
+
+
+        5.3.3 DISTRIBUTION OF EXTERNAL MODULES
+
+When the Licensee has developed an External Module, the terms and
+conditions of this Agreement do not apply to said External Module, that
+may be distributed under a separate license agreement.
+
+
+        5.3.4 COMPATIBILITY WITH THE GNU GPL
+
+The Licensee can include a code that is subject to the provisions of one
+of the versions of the GNU GPL in the Modified or unmodified Software,
+and distribute that entire code under the terms of the same version of
+the GNU GPL.
+
+The Licensee can include the Modified or unmodified Software in a code
+that is subject to the provisions of one of the versions of the GNU GPL,
+and distribute that entire code under the terms of the same version of
+the GNU GPL.
+
+
+    Article 6 - INTELLECTUAL PROPERTY
+
+
+      6.1 OVER THE INITIAL SOFTWARE
+
+The Holder owns the economic rights over the Initial Software. Any or
+all use of the Initial Software is subject to compliance with the terms
+and conditions under which the Holder has elected to distribute its work
+and no one shall be entitled to modify the terms and conditions for the
+distribution of said Initial Software.
+
+The Holder undertakes that the Initial Software will remain ruled at
+least by this Agreement, for the duration set forth in Article 4.2.
+
+
+      6.2 OVER THE CONTRIBUTIONS
+
+The Licensee who develops a Contribution is the owner of the
+intellectual property rights over this Contribution as defined by
+applicable law.
+
+
+      6.3 OVER THE EXTERNAL MODULES
+
+The Licensee who develops an External Module is the owner of the
+intellectual property rights over this External Module as defined by
+applicable law and is free to choose the type of agreement that shall
+govern its distribution.
+
+
+      6.4 JOINT PROVISIONS
+
+The Licensee expressly undertakes:
+
+   1. not to remove, or modify, in any manner, the intellectual property
+      notices attached to the Software;
+
+   2. to reproduce said notices, in an identical manner, in the copies
+      of the Software modified or not.
+
+The Licensee undertakes not to directly or indirectly infringe the
+intellectual property rights of the Holder and/or Contributors on the
+Software and to take, where applicable, vis-�-vis its staff, any and all
+measures required to ensure respect of said intellectual property rights
+of the Holder and/or Contributors.
+
+
+    Article 7 - RELATED SERVICES
+
+7.1 Under no circumstances shall the Agreement oblige the Licensor to
+provide technical assistance or maintenance services for the Software.
+
+However, the Licensor is entitled to offer this type of services. The
+terms and conditions of such technical assistance, and/or such
+maintenance, shall be set forth in a separate instrument. Only the
+Licensor offering said maintenance and/or technical assistance services
+shall incur liability therefor.
+
+7.2 Similarly, any Licensor is entitled to offer to its licensees, under
+its sole responsibility, a warranty, that shall only be binding upon
+itself, for the redistribution of the Software and/or the Modified
+Software, under terms and conditions that it is free to decide. Said
+warranty, and the financial terms and conditions of its application,
+shall be subject of a separate instrument executed between the Licensor
+and the Licensee.
+
+
+    Article 8 - LIABILITY
+
+8.1 Subject to the provisions of Article 8.2, the Licensee shall be
+entitled to claim compensation for any direct loss it may have suffered
+from the Software as a result of a fault on the part of the relevant
+Licensor, subject to providing evidence thereof.
+
+8.2 The Licensor's liability is limited to the commitments made under
+this Agreement and shall not be incurred as a result of in particular:
+(i) loss due the Licensee's total or partial failure to fulfill its
+obligations, (ii) direct or consequential loss that is suffered by the
+Licensee due to the use or performance of the Software, and (iii) more
+generally, any consequential loss. In particular the Parties expressly
+agree that any or all pecuniary or business loss (i.e. loss of data,
+loss of profits, operating loss, loss of customers or orders,
+opportunity cost, any disturbance to business activities) or any or all
+legal proceedings instituted against the Licensee by a third party,
+shall constitute consequential loss and shall not provide entitlement to
+any or all compensation from the Licensor.
+
+
+    Article 9 - WARRANTY
+
+9.1 The Licensee acknowledges that the scientific and technical
+state-of-the-art when the Software was distributed did not enable all
+possible uses to be tested and verified, nor for the presence of
+possible defects to be detected. In this respect, the Licensee's
+attention has been drawn to the risks associated with loading, using,
+modifying and/or developing and reproducing the Software which are
+reserved for experienced users.
+
+The Licensee shall be responsible for verifying, by any or all means,
+the suitability of the product for its requirements, its good working
+order, and for ensuring that it shall not cause damage to either persons
+or properties.
+
+9.2 The Licensor hereby represents, in good faith, that it is entitled
+to grant all the rights over the Software (including in particular the
+rights set forth in Article 5).
+
+9.3 The Licensee acknowledges that the Software is supplied "as is" by
+the Licensor without any other express or tacit warranty, other than
+that provided for in Article 9.2 and, in particular, without any warranty 
+as to its commercial value, its secured, safe, innovative or relevant
+nature.
+
+Specifically, the Licensor does not warrant that the Software is free
+from any error, that it will operate without interruption, that it will
+be compatible with the Licensee's own equipment and software
+configuration, nor that it will meet the Licensee's requirements.
+
+9.4 The Licensor does not either expressly or tacitly warrant that the
+Software does not infringe any third party intellectual property right
+relating to a patent, software or any other property right. Therefore,
+the Licensor disclaims any and all liability towards the Licensee
+arising out of any or all proceedings for infringement that may be
+instituted in respect of the use, modification and redistribution of the
+Software. Nevertheless, should such proceedings be instituted against
+the Licensee, the Licensor shall provide it with technical and legal
+assistance for its defense. Such technical and legal assistance shall be
+decided on a case-by-case basis between the relevant Licensor and the
+Licensee pursuant to a memorandum of understanding. The Licensor
+disclaims any and all liability as regards the Licensee's use of the
+name of the Software. No warranty is given as regards the existence of
+prior rights over the name of the Software or as regards the existence
+of a trademark.
+
+
+    Article 10 - TERMINATION
+
+10.1 In the event of a breach by the Licensee of its obligations
+hereunder, the Licensor may automatically terminate this Agreement
+thirty (30) days after notice has been sent to the Licensee and has
+remained ineffective.
+
+10.2 A Licensee whose Agreement is terminated shall no longer be
+authorized to use, modify or distribute the Software. However, any
+licenses that it may have granted prior to termination of the Agreement
+shall remain valid subject to their having been granted in compliance
+with the terms and conditions hereof.
+
+
+    Article 11 - MISCELLANEOUS
+
+
+      11.1 EXCUSABLE EVENTS
+
+Neither Party shall be liable for any or all delay, or failure to
+perform the Agreement, that may be attributable to an event of force
+majeure, an act of God or an outside cause, such as defective
+functioning or interruptions of the electricity or telecommunications
+networks, network paralysis following a virus attack, intervention by
+government authorities, natural disasters, water damage, earthquakes,
+fire, explosions, strikes and labor unrest, war, etc.
+
+11.2 Any failure by either Party, on one or more occasions, to invoke
+one or more of the provisions hereof, shall under no circumstances be
+interpreted as being a waiver by the interested Party of its right to
+invoke said provision(s) subsequently.
+
+11.3 The Agreement cancels and replaces any or all previous agreements,
+whether written or oral, between the Parties and having the same
+purpose, and constitutes the entirety of the agreement between said
+Parties concerning said purpose. No supplement or modification to the
+terms and conditions hereof shall be effective as between the Parties
+unless it is made in writing and signed by their duly authorized
+representatives.
+
+11.4 In the event that one or more of the provisions hereof were to
+conflict with a current or future applicable act or legislative text,
+said act or legislative text shall prevail, and the Parties shall make
+the necessary amendments so as to comply with said act or legislative
+text. All other provisions shall remain effective. Similarly, invalidity
+of a provision of the Agreement, for any reason whatsoever, shall not
+cause the Agreement as a whole to be invalid.
+
+
+      11.5 LANGUAGE
+
+The Agreement is drafted in both French and English and both versions
+are deemed authentic.
+
+
+    Article 12 - NEW VERSIONS OF THE AGREEMENT
+
+12.1 Any person is authorized to duplicate and distribute copies of this
+Agreement.
+
+12.2 So as to ensure coherence, the wording of this Agreement is
+protected and may only be modified by the authors of the License, who
+reserve the right to periodically publish updates or new versions of the
+Agreement, each with a separate number. These subsequent versions may
+address new issues encountered by Free Software.
+
+12.3 Any Software distributed under a given version of the Agreement may
+only be subsequently distributed under the same version of the Agreement
+or a subsequent version, subject to the provisions of Article 5.3.4.
+
+
+    Article 13 - GOVERNING LAW AND JURISDICTION
+
+13.1 The Agreement is governed by French law. The Parties agree to
+endeavor to seek an amicable solution to any disagreements or disputes
+that may arise during the performance of the Agreement.
+
+13.2 Failing an amicable solution within two (2) months as from their
+occurrence, and unless emergency proceedings are necessary, the
+disagreements or disputes shall be referred to the Paris Courts having
+jurisdiction, by the more diligent Party.
+
+
+Version 2.0 dated 2006-09-05.
diff --git a/trunk/INSTALL b/trunk/INSTALL
new file mode 100644 (file)
index 0000000..3341d55
--- /dev/null
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2007-2009 Guillaume Pellerin <pellerin@parisson.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://svn.parisson.org/deefuzzer/DeeFuzzLicense.
+#
+# Author: Guillaume Pellerin <pellerin@parisson.com>
+
+ depends:  python, python-dev, python-xml, python-shout | shout-python, libshout3,
+           libshout3-dev, python-mutagen
+
+ provides:  shout-python
+
+ optional: python-twitter, python-tinyurl, python-liblo | pyliblo (>= 0.26)
+
+ recommends: icecast2
+
+python-shout is included in the DeeFuzzer but needs to be compiled and installed
+if you don't install deefuzzer from its debian package with aptitude.
+As explained in shout-python/INSTALL, you just have to run this command :
+
+  $ cd shout-python
+  $ sudo python setup.py install
+
+To install the DeeFuzzer (from the main deefuzzer directory) :
+
+  $ sudo python install.py
+
+For more informations, see http://svn.parisson.org/deefuzzer/
\ No newline at end of file
diff --git a/trunk/README b/trunk/README
new file mode 100644 (file)
index 0000000..ff36bbd
--- /dev/null
@@ -0,0 +1,147 @@
+================
+DeeFuzzer README
+================
+
+deefuzzer : an easy and instant media streaming tool
+
+
+Introduction
+============
+
+DeeFuzzer is a new light and instant software made for streaming audio and video over internet. It is dedicated to people who wants to create playlisted web radios or web TVs with rich media contents including some metadata.
+
+Here are the main features of the deefuzzer:
+
+ * MP3 and OGG (audio & video) file streaming over internet (Icecast)
+ * Full metadata encapsulation and management
+ * RSS podcast generator (current tracks and playlists)
+ * M3U playlist generator
+ * Recursive, random (shuffled) or pre-defined playlists
+ * Multi-threaded architecture : multiple station streaming with one config file
+ * Auto Twitter posting of the current playing tracks
+ * Jingling between main tracks
+ * OSC controller : control the main functions from a distant terminal
+ * Station relaying : stream other stations like *LIVE* sessions !
+ * Very light and optimized streaming process
+
+It is neccessary to provide a config file which sets all needed parameters
+Please see example/myfuzz.xml for an example.
+
+
+Installation
+============
+
+see INSTALL
+
+
+License
+=======
+
+This software is licensed as described in the file COPYING, which
+you should have received as part of this distribution. The terms
+are also available at http://svn.parisson.org/deefuzzer/DeeFuzzerLicense
+
+
+Usage
+=====
+
+Usage : deefuzzer CONFIGFILE
+
+where CONFIGFILE is the path for a XML config file. For example::
+
+$ deefuzzer example/myfuzz.xml
+
+To make the deefuzzer act as a deamon, just play it in the background::
+
+$ deefuzzer example/myfuzz.xml &
+
+Note that you must edit the config file with right parameters before playing.
+You can find an example for a draft XML file in the directory "example/" of this
+application (maybe in /usr/share/deefuzzer/example/ if installed with the help of install.py)
+
+Since 0.3, deefuzzer doesn't print anything into the shell, then a right <log> parameter
+is needed in the XML config file.
+
+BE CAREFULL : at the moment, the multi-threaded implementation of deefuzzer instances
+avoids shutting down the streams with CTRL+C... You have to kill them manually,
+after a CTRL+Z, making this::
+
+$ kill -9 PROCESS_ID
+
+or::
+
+$ kill -9 `pgrep deefuzzer`
+
+
+XML Configuration
+=================
+
+See example/myfuzz.xml in a text editor.
+The inline comments should help you to configure your stations.
+
+
+OSC Control
+===========
+
+We can now control some functions of the deefuzzer with external commands
+coming from an OSC client. These are accessible only if the "control" tag is
+set up in the config file (see example/myfuzz.xml again...).
+
+Next track::
+
+$ python tools/osc_next.py
+
+Start twitting::
+
+$ python tools/osc_twitter_start.py
+
+Stop twitting::
+
+$ python see tools/osc_twitter_stop.py
+
+Start relaying::
+
+$ python tools/osc_relay_start.py
+
+Stop relaying::
+
+$ python see tools/osc_relay_stop.py
+
+Start jingling::
+
+$ python see tools/osc_jingles_start.py
+
+Stop jingling::
+
+$ python see tools/osc_jingles_stop.py
+
+
+Author
+======
+
+Guillaume Pellerin <yomguy@parisson.com>
+
+
+Licence
+=======
+
+See COPYING
+
+
+Aknowledgements
+===============
+
+This work is inspired by the great - C coded - Oddsock's streaming program : Ezstream.
+Since I needed to patch it in order to modify the playlist (randomize for example)
+and make external batch tools to create multiple channels, I decided to rewrite it
+from scratch in python.
+
+Some parts of this work are also taken from another Parisson's project : Telemeta
+(see http://telemeta.org).
+
+
+Contact / Infos
+===============
+
+see http://svn.parisson.org/deefuzzer/ or http://parisson.com for more info.
+
diff --git a/trunk/__init__.py b/trunk/__init__.py
new file mode 100644 (file)
index 0000000..87b3ced
--- /dev/null
@@ -0,0 +1,2 @@
+from deefuzzer import *
+
diff --git a/trunk/debian/README.Debian b/trunk/debian/README.Debian
new file mode 100644 (file)
index 0000000..adbf3a6
--- /dev/null
@@ -0,0 +1,6 @@
+d-fuzz for Debian
+-----------------
+
+<possible notes regarding this package - if none, delete this file>
+
+ -- G. Pellerin <yomguy@altern.org>  Sun, 09 Dec 2007 21:50:43 +0100
diff --git a/trunk/debian/changelog b/trunk/debian/changelog
new file mode 100644 (file)
index 0000000..87bf132
--- /dev/null
@@ -0,0 +1,19 @@
+d-fuzz (0.3.3-1) unstable; urgency=low
+
+  * add twitter api
+  * cleanup RSS and song metadata
+
+ -- momo <momo@wm16.parisson.org>  Tue, 27 Oct 2009 13:44:06 +0100
+
+d-fuzz (0.3.0-1) unstable; urgency=low
+
+  * new and much upgraded DeeFuzz
+
+ -- Guillaume Pellerin <yomguy@>  Fri, 17 Apr 2009 09:48:25 +0200
+
+d-fuzz (0.2-1) unstable; urgency=low
+
+  * Initial release
+
+ -- G. Pellerin <yomguy@altern.org>  Sun, 09 Dec 2007 21:50:43 +0100
+
diff --git a/trunk/debian/compat b/trunk/debian/compat
new file mode 100644 (file)
index 0000000..7ed6ff8
--- /dev/null
@@ -0,0 +1 @@
+5
diff --git a/trunk/debian/control b/trunk/debian/control
new file mode 100644 (file)
index 0000000..12168bd
--- /dev/null
@@ -0,0 +1,25 @@
+Source: deefuzz
+Section: sound
+Priority: optional
+Maintainer: Guillaume Pellerin <yomguy@altern.org>
+Build-Depends: debhelper (>= 5), python
+Standards-Version: 3.7.2
+
+Package: deefuzzer
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}, icecast2, python-xml, python-shout, libshout3, python-mutagen
+Recommends: python-twitter, python-tinyurl, python-liblo | pyliblo (>= 0.26)
+Description: easy and light media streaming tool
+ DeeFuzz Tools are new light and easy tools to stream audio and video over internet. It is dedicated to people who wants to create playlisted webradios or webTVs with rich media contents. Here are the main features of the DeeFuzz Tools:
+ * MP3 and OGG (audio & video) file streaming over internet (Icecast)
+ * Full metadata encapsulation and management
+ * RSS podcast generator (current tracks and playlists)
+ * M3U playlist generator
+ * Recursive, random (shuffled) or pre-defined playlists
+ * Multi-threaded architecture : multiple station streaming with one config file
+ * Auto Twitter posting of the current playing tracks
+ * Jingling between main tracks
+ * OSC controller : control the main functions from a distant terminal
+ * Station relaying : stream other stations like *LIVE* sessions !
+ * Very light and optimized streaming process
+ See http://svn.parisson.org/deefuzzer/ or http://parisson.com for more info.
diff --git a/trunk/debian/copyright b/trunk/debian/copyright
new file mode 100644 (file)
index 0000000..3810e18
--- /dev/null
@@ -0,0 +1,49 @@
+This package was debianized by Guillaume Pellerin <yomguy@parisson.com> on
+Sun, 09 Dec 2007 21:50:43 +0100.
+
+It was downloaded from http://svn.parisson.org/deefuzz
+
+Upstream Author(s): 
+
+    Guillaume Pellerin <yomguy@parisson.com>
+
+Copyright: 
+
+    Copyright (C) 2007-2009 Guillaume Pellerin
+
+License:
+
+    This software is a computer program whose purpose is to stream audio
+    and video data through icecast2 servers.
+
+    This software is governed by the CeCILL  license under French law and
+    abiding by the rules of distribution of free software.  You can  use,
+    modify and/ or redistribute the software under the terms of the CeCILL
+    license as circulated by CEA, CNRS and INRIA at the following URL
+    "http://www.cecill.info".
+
+    As a counterpart to the access to the source code and  rights to copy,
+    modify and redistribute granted by the license, users are provided only
+    with a limited warranty  and the software's author,  the holder of the
+    economic rights,  and the successive licensors  have only  limited
+    liability.
+
+    In this respect, the user's attention is drawn to the risks associated
+    with loading,  using,  modifying and/or developing or reproducing the
+    software by the user in light of its specific status of free software,
+    that may mean  that it is complicated to manipulate,  and  that  also
+    therefore means  that it is reserved for developers  and  experienced
+    professionals having in-depth computer knowledge. Users are therefore
+    encouraged to load and test the software's suitability as regards their
+    requirements in conditions enabling the security of their systems and/or
+    data to be ensured and,  more generally, to use and operate it in the
+    same conditions as regards security.
+
+    The fact that you are presently reading this means that you have had
+    knowledge of the CeCILL license and that you accept its terms.
+
+    The Debian packaging is (C) 2009, Guillaume Pellerin <yomguy@parisson.com>
+    and is licensed under the CeCILL v2 licence, see http://www.cecill.info.
+
+    Please also look if there are files or directories which have a
+    different copyright/license attached and list them here.
diff --git a/trunk/debian/dirs b/trunk/debian/dirs
new file mode 100644 (file)
index 0000000..69445f1
--- /dev/null
@@ -0,0 +1,3 @@
+usr/bin
+usr/share
+
diff --git a/trunk/debian/docs b/trunk/debian/docs
new file mode 100644 (file)
index 0000000..e845566
--- /dev/null
@@ -0,0 +1 @@
+README
diff --git a/trunk/debian/manpage.1.ex b/trunk/debian/manpage.1.ex
new file mode 100644 (file)
index 0000000..62cd2f5
--- /dev/null
@@ -0,0 +1,59 @@
+.\"                                      Hey, EMACS: -*- nroff -*-
+.\" First parameter, NAME, should be all caps
+.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
+.\" other parameters are allowed: see man(7), man(1)
+.TH D-FUZZ SECTION "décembre  9, 2007"
+.\" Please adjust this date whenever revising the manpage.
+.\"
+.\" Some roff macros, for reference:
+.\" .nh        disable hyphenation
+.\" .hy        enable hyphenation
+.\" .ad l      left justify
+.\" .ad b      justify to both left and right margins
+.\" .nf        disable filling
+.\" .fi        enable filling
+.\" .br        insert line break
+.\" .sp <n>    insert n+1 empty lines
+.\" for manpage-specific macros, see man(7)
+.SH NAME
+deefuzz \- program to do something
+.SH SYNOPSIS
+.B deefuzz
+.RI [ options ] " files" ...
+.br
+.B bar
+.RI [ options ] " files" ...
+.SH DESCRIPTION
+This manual page documents briefly the
+.B deefuzz
+and
+.B bar
+commands.
+.PP
+.\" TeX users may be more comfortable with the \fB<whatever>\fP and
+.\" \fI<whatever>\fP escape sequences to invode bold face and italics, 
+.\" respectively.
+\fBdeefuzz\fP is a program that...
+.SH OPTIONS
+These programs follow the usual GNU command line syntax, with long
+options starting with two dashes (`-').
+A summary of options is included below.
+For a complete description, see the Info files.
+.TP
+.B \-h, \-\-help
+Show summary of options.
+.TP
+.B \-v, \-\-version
+Show version of program.
+.SH SEE ALSO
+.BR bar (1),
+.BR baz (1).
+.br
+The programs are documented fully by
+.IR "The Rise and Fall of a Fooish Bar" ,
+available via the Info system.
+.SH AUTHOR
+deefuzz was written by <upstream author>.
+.PP
+This manual page was written by G. Pellerin <yomguy@altern.org>,
+for the Debian project (but may be used by others).
diff --git a/trunk/debian/manpage.sgml.ex b/trunk/debian/manpage.sgml.ex
new file mode 100644 (file)
index 0000000..625e093
--- /dev/null
@@ -0,0 +1,156 @@
+<!doctype refentry PUBLIC "-//OASIS//DTD DocBook V4.1//EN" [
+
+<!-- Process this file with docbook-to-man to generate an nroff manual
+     page: `docbook-to-man manpage.sgml > manpage.1'.  You may view
+     the manual page with: `docbook-to-man manpage.sgml | nroff -man |
+     less'.  A typical entry in a Makefile or Makefile.am is:
+
+manpage.1: manpage.sgml
+       docbook-to-man $< > $@
+
+    
+       The docbook-to-man binary is found in the docbook-to-man package.
+       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
+       docbook-to-man in your Build-Depends control field.
+
+  -->
+
+  <!-- Fill in your name for FIRSTNAME and SURNAME. -->
+  <!ENTITY dhfirstname "<firstname>FIRSTNAME</firstname>">
+  <!ENTITY dhsurname   "<surname>SURNAME</surname>">
+  <!-- Please adjust the date whenever revising the manpage. -->
+  <!ENTITY dhdate      "<date>décembre  9, 2007</date>">
+  <!-- SECTION should be 1-8, maybe w/ subsection other parameters are
+       allowed: see man(7), man(1). -->
+  <!ENTITY dhsection   "<manvolnum>SECTION</manvolnum>">
+  <!ENTITY dhemail     "<email>yomguy@altern.org</email>">
+  <!ENTITY dhusername  "G. Pellerin">
+  <!ENTITY dhucpackage "<refentrytitle>D-FUZZ</refentrytitle>">
+  <!ENTITY dhpackage   "deefuzz">
+
+  <!ENTITY debian      "<productname>Debian</productname>">
+  <!ENTITY gnu         "<acronym>GNU</acronym>">
+  <!ENTITY gpl         "&gnu; <acronym>GPL</acronym>">
+]>
+
+<refentry>
+  <refentryinfo>
+    <address>
+      &dhemail;
+    </address>
+    <author>
+      &dhfirstname;
+      &dhsurname;
+    </author>
+    <copyright>
+      <year>2003</year>
+      <holder>&dhusername;</holder>
+    </copyright>
+    &dhdate;
+  </refentryinfo>
+  <refmeta>
+    &dhucpackage;
+
+    &dhsection;
+  </refmeta>
+  <refnamediv>
+    <refname>&dhpackage;</refname>
+
+    <refpurpose>program to do something</refpurpose>
+  </refnamediv>
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>&dhpackage;</command>
+
+      <arg><option>-e <replaceable>this</replaceable></option></arg>
+
+      <arg><option>--example <replaceable>that</replaceable></option></arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+  <refsect1>
+    <title>DESCRIPTION</title>
+
+    <para>This manual page documents briefly the
+      <command>&dhpackage;</command> and <command>bar</command>
+      commands.</para>
+
+    <para>This manual page was written for the &debian; distribution
+      because the original program does not have a manual page.
+      Instead, it has documentation in the &gnu;
+      <application>Info</application> format; see below.</para>
+
+    <para><command>&dhpackage;</command> is a program that...</para>
+
+  </refsect1>
+  <refsect1>
+    <title>OPTIONS</title>
+
+    <para>These programs follow the usual &gnu; command line syntax,
+      with long options starting with two dashes (`-').  A summary of
+      options is included below.  For a complete description, see the
+      <application>Info</application> files.</para>
+
+    <variablelist>
+      <varlistentry>
+        <term><option>-h</option>
+          <option>--help</option>
+        </term>
+        <listitem>
+          <para>Show summary of options.</para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term><option>-v</option>
+          <option>--version</option>
+        </term>
+        <listitem>
+          <para>Show version of program.</para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+  <refsect1>
+    <title>SEE ALSO</title>
+
+    <para>bar (1), baz (1).</para>
+
+    <para>The programs are documented fully by <citetitle>The Rise and
+      Fall of a Fooish Bar</citetitle> available via the
+      <application>Info</application> system.</para>
+  </refsect1>
+  <refsect1>
+    <title>AUTHOR</title>
+
+    <para>This manual page was written by &dhusername; &dhemail; for
+      the &debian; system (but may be used by others).  Permission is
+      granted to copy, distribute and/or modify this document under
+      the terms of the &gnu; General Public License, Version 2 any 
+         later version published by the Free Software Foundation.
+    </para>
+       <para>
+         On Debian systems, the complete text of the GNU General Public
+         License can be found in /usr/share/common-licenses/GPL.
+       </para>
+
+  </refsect1>
+</refentry>
+
+<!-- Keep this comment at the end of the file
+Local variables:
+mode: sgml
+sgml-omittag:t
+sgml-shorttag:t
+sgml-minimize-attributes:nil
+sgml-always-quote-attributes:t
+sgml-indent-step:2
+sgml-indent-data:t
+sgml-parent-document:nil
+sgml-default-dtd-file:nil
+sgml-exposed-tags:nil
+sgml-local-catalogs:nil
+sgml-local-ecat-files:nil
+End:
+-->
+
+
diff --git a/trunk/debian/manpage.xml.ex b/trunk/debian/manpage.xml.ex
new file mode 100644 (file)
index 0000000..c6e9a7c
--- /dev/null
@@ -0,0 +1,144 @@
+<?xml version='1.0' encoding='ISO-8859-1'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd" [
+
+<!--
+
+Process this file with an XSLT processor: `xsltproc \
+-''-nonet /usr/share/sgml/docbook/stylesheet/xsl/nwalsh/\
+manpages/docbook.xsl manpage.dbk'.  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
+
+manpage.1: manpage.dbk
+        $(XP) $(DB2MAN) $<
+    
+The xsltproc binary is found in the xsltproc package.  The
+XSL files are in docbook-xsl.  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.
+
+-->
+
+  <!-- Fill in your name for FIRSTNAME and SURNAME. -->
+  <!ENTITY dhfirstname "<firstname>FIRSTNAME</firstname>">
+  <!ENTITY dhsurname   "<surname>SURNAME</surname>">
+  <!-- Please adjust the date whenever revising the manpage. -->
+  <!ENTITY dhdate      "<date>décembre  9, 2007</date>">
+  <!-- SECTION should be 1-8, maybe w/ subsection other parameters are
+       allowed: see man(7), man(1). -->
+  <!ENTITY dhsection   "<manvolnum>SECTION</manvolnum>">
+  <!ENTITY dhemail     "<email>yomguy@altern.org</email>">
+  <!ENTITY dhusername  "G. Pellerin">
+  <!ENTITY dhucpackage "<refentrytitle>D-FUZZ</refentrytitle>">
+  <!ENTITY dhpackage   "deefuzz">
+
+  <!ENTITY debian      "<productname>Debian</productname>">
+  <!ENTITY gnu         "<acronym>GNU</acronym>">
+  <!ENTITY gpl         "&gnu; <acronym>GPL</acronym>">
+]>
+
+<refentry>
+  <refentryinfo>
+    <address>
+      &dhemail;
+    </address>
+    <copyright>
+      <year>2007</year>
+      <holder>&dhusername;</holder>
+    </copyright>
+    &dhdate;
+  </refentryinfo>
+  <refmeta>
+    &dhucpackage;
+
+    &dhsection;
+  </refmeta>
+  <refnamediv>
+    <refname>&dhpackage;</refname>
+
+    <refpurpose>program to do something</refpurpose>
+  </refnamediv>
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>&dhpackage;</command>
+
+      <arg><option>-e <replaceable>this</replaceable></option></arg>
+
+      <arg><option>--example <replaceable>that</replaceable></option></arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+  <refsect1>
+    <title>DESCRIPTION</title>
+
+    <para>This manual page documents briefly the
+      <command>&dhpackage;</command> and <command>bar</command>
+      commands.</para>
+
+    <para>This manual page was written for the &debian; distribution
+      because the original program does not have a manual page.
+      Instead, it has documentation in the &gnu;
+      <application>Info</application> format; see below.</para>
+
+    <para><command>&dhpackage;</command> is a program that...</para>
+
+  </refsect1>
+  <refsect1>
+    <title>OPTIONS</title>
+
+    <para>These programs follow the usual &gnu; command line syntax,
+      with long options starting with two dashes (`-').  A summary of
+      options is included below.  For a complete description, see the
+      <application>Info</application> files.</para>
+
+    <variablelist>
+      <varlistentry>
+        <term><option>-h</option>
+          <option>--help</option>
+        </term>
+        <listitem>
+          <para>Show summary of options.</para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term><option>-v</option>
+          <option>--version</option>
+        </term>
+        <listitem>
+          <para>Show version of program.</para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+  <refsect1>
+    <title>SEE ALSO</title>
+
+    <para>bar (1), baz (1).</para>
+
+    <para>The programs are documented fully by <citetitle>The Rise and
+      Fall of a Fooish Bar</citetitle> available via the
+      <application>Info</application> system.</para>
+  </refsect1>
+  <refsect1>
+    <title>AUTHOR</title>
+
+    <para>This manual page was written by &dhusername; &dhemail; for
+      the &debian; system (but may be used by others).  Permission is
+      granted to copy, distribute and/or modify this document under
+      the terms of the &gnu; General Public License, Version 2 any 
+         later version published by the Free Software Foundation.
+    </para>
+       <para>
+         On Debian systems, the complete text of the GNU General Public
+         License can be found in /usr/share/common-licenses/GPL.
+       </para>
+
+  </refsect1>
+</refentry>
+
diff --git a/trunk/debian/rules b/trunk/debian/rules
new file mode 100755 (executable)
index 0000000..6fbdb3c
--- /dev/null
@@ -0,0 +1,7 @@
+#!/usr/bin/make -f
+  
+include /usr/share/cdbs/1/rules/debhelper.mk
+include /usr/share/cdbs/1/class/makefile.mk
+include /usr/share/cdbs/1/class/python-distutils.mk
+
+# Add here any variable or target overrides you need.
diff --git a/trunk/deefuzzer.e4p b/trunk/deefuzzer.e4p
new file mode 100644 (file)
index 0000000..e6a78f5
--- /dev/null
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Project SYSTEM "Project-4.6.dtd">
+<!-- eric4 project file for project deefuzzer -->
+<!-- Saved: 2010-09-09, 15:51:44 -->
+<!-- Copyright (C) 2010 Guillaume Pellerin, yomguy@parisson.com -->
+<Project version="4.6">
+  <Language>en</Language>
+  <ProgLanguage mixed="0">Python</ProgLanguage>
+  <ProjectType>Other</ProjectType>
+  <Description>DeeFuzzer is a new light and instant software made for streaming audio and video over internet. It is dedicated to people who wants to create playlisted web radios or web TVs with rich media contents including some metadata.</Description>
+  <Version>0.4.2</Version>
+  <Author>Guillaume Pellerin</Author>
+  <Email>yomguy@parisson.com</Email>
+  <Sources>
+    <Source>tools/xmltodict2.py</Source>
+    <Source>tools/PyRSS2Gen.py</Source>
+    <Source>tools/ogg.py</Source>
+    <Source>tools/__init__.py</Source>
+    <Source>tools/logger.py</Source>
+    <Source>tools/mp3.py</Source>
+    <Source>tools/tools.py</Source>
+    <Source>tools/xmltodict.py</Source>
+    <Source>tools/player.py</Source>
+    <Source>tools/relay.py</Source>
+    <Source>tools/osc.py</Source>
+    <Source>tools/osc_twitter_start.py</Source>
+    <Source>tools/twitt.py</Source>
+    <Source>tools/osc_twitter_stop.py</Source>
+    <Source>tools/osc_jingles_start.py</Source>
+    <Source>tools/osc_jingles_stop.py</Source>
+    <Source>tools/recorder.py</Source>
+    <Source>tools/osc_record_start.py</Source>
+    <Source>tools/osc_record_stop.py</Source>
+    <Source>tools/osc_player_slow.py</Source>
+    <Source>tools/osc_player_fast.py</Source>
+    <Source>tools/osc_player_next.py</Source>
+    <Source>tools/osc_run_stop.py</Source>
+    <Source>tools/osc_player_next2.py</Source>
+    <Source>tools/osc_relay_start.py</Source>
+    <Source>tools/osc_relay_stop.py</Source>
+    <Source>tools/station.py</Source>
+    <Source>tools/get_access_token.py</Source>
+    <Source>shout-python/example.py</Source>
+    <Source>shout-python/setup.py</Source>
+    <Source>example/deefuzz_xml_gen.py</Source>
+    <Source>deefuzzer.py</Source>
+    <Source>__init__.py</Source>
+    <Source>install.py</Source>
+    <Source>tests/test_twitter2.py</Source>
+  </Sources>
+  <Forms>
+  </Forms>
+  <Translations>
+  </Translations>
+  <Resources>
+  </Resources>
+  <Interfaces>
+  </Interfaces>
+  <Others>
+  </Others>
+  <Vcs>
+    <VcsType>Subversion</VcsType>
+    <VcsOptions>
+      <dict>
+        <key>
+          <string>add</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+        <key>
+          <string>checkout</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+        <key>
+          <string>commit</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+        <key>
+          <string>diff</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+        <key>
+          <string>export</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+        <key>
+          <string>global</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+        <key>
+          <string>history</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+        <key>
+          <string>log</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+        <key>
+          <string>remove</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+        <key>
+          <string>status</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+        <key>
+          <string>tag</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+        <key>
+          <string>update</string>
+        </key>
+        <value>
+          <list>
+          </list>
+        </value>
+      </dict>
+    </VcsOptions>
+    <VcsOtherData>
+      <dict>
+        <key>
+          <string>standardLayout</string>
+        </key>
+        <value>
+          <bool>True</bool>
+        </value>
+      </dict>
+    </VcsOtherData>
+  </Vcs>
+  <FiletypeAssociations>
+    <FiletypeAssociation pattern="*.pyw" type="SOURCES" />
+    <FiletypeAssociation pattern="*.idl" type="INTERFACES" />
+    <FiletypeAssociation pattern="*.py" type="SOURCES" />
+    <FiletypeAssociation pattern="*.ptl" type="SOURCES" />
+  </FiletypeAssociations>
+</Project>
\ No newline at end of file
diff --git a/trunk/deefuzzer.py b/trunk/deefuzzer.py
new file mode 100755 (executable)
index 0000000..3155169
--- /dev/null
@@ -0,0 +1,176 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Guillaume Pellerin
+
+# <yomguy@parisson.com>
+
+# This software is a computer program whose purpose is to stream audio
+# and video data through icecast2 servers.
+
+# This software is governed by the CeCILL license under French law and
+# abiding by the rules of distribution of free software. You can use,
+# modify and/ or redistribute the software under the terms of the CeCILL
+# license as circulated by CEA, CNRS and INRIA at the following URL
+# "http://www.cecill.info".
+
+# As a counterpart to the access to the source code and  rights to copy,
+# modify and redistribute granted by the license, users are provided only
+# with a limited warranty and the software's author, the holder of the
+# economic rights, and the successive licensors have only limited
+# liability.
+
+# In this respect, the user's attention is drawn to the risks associated
+# with loading, using,  modifying and/or developing or reproducing the
+# software by the user in light of its specific status of free software,
+# that may mean that it is complicated to manipulate, and that also
+# therefore means that it is reserved for developers and  experienced
+# professionals having in-depth computer knowledge. Users are therefore
+# encouraged to load and test the software's suitability as regards their
+# requirements in conditions enabling the security of their systems and/or
+# data to be ensured and, more generally, to use and operate it in the
+# same conditions as regards security.
+
+# The fact that you are presently reading this means that you have had
+# knowledge of the CeCILL license and that you accept its terms.
+
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+import os
+import sys
+import shout
+import Queue
+import datetime
+import platform
+from threading import Thread
+from tools import *
+
+version = '0.4.2'
+year = datetime.datetime.now().strftime("%Y")
+platform_system = platform.system()
+
+
+def prog_info():
+    desc = """ deefuzzer : easy and instant media streaming tool
+ version : %s
+ running on system : %s
+
+ Copyright (c) 2007-%s Guillaume Pellerin <yomguy@parisson.com>
+ All rights reserved.
+
+ This software is licensed as described in the file COPYING, which
+ you should have received as part of this distribution. The terms
+ are also available at http://svn.parisson.org/deefuzzer/DeeFuzzerLicense
+
+ depends:  python, python-dev, python-xml, python-shout | shout-python, libshout3,
+           libshout3-dev, icecast2, python-mutagen
+
+ recommends: python-twitter, python-tinyurl, python-liblo | pyliblo (>= 0.26)
+
+ provides:  shout-python
+
+ Usage : deefuzzer $1
+  where $1 is the path for a XML config file
+  ex: deefuzzer example/myfuzz.xml
+
+ see http://svn.parisson.org/deefuzzer/ for more details
+        """
+
+    return desc % (version, platform_system, year)
+
+
+class DeeFuzzer(Thread):
+    """a DeeFuzzer diffuser"""
+
+    def __init__(self, conf_file):
+        Thread.__init__(self)
+        self.conf_file = conf_file
+        self.conf = self.get_conf_dict()
+        if 'log' in self.conf['deefuzzer'].keys():
+            self.logger = Logger(self.conf['deefuzzer']['log'])
+        else:
+            self.logger = Logger('.' + os.sep + 'deefuzzer.log')
+        if 'm3u' in self.conf['deefuzzer'].keys():
+            self.m3u = self.conf['deefuzzer']['m3u']
+        else:
+            self.m3u = '.' + os.sep + 'deefuzzer.m3u'
+
+        if isinstance(self.conf['deefuzzer']['station'], dict):
+            # Fix wrong type data from xmltodict when one station (*)
+            self.nb_stations = 1
+        else:
+            self.nb_stations = len(self.conf['deefuzzer']['station'])
+
+        # Set the deefuzzer logger
+        self.logger.write_info('Starting DeeFuzzer v' + version)
+        self.logger.write_info('Using libshout version %s' % shout.version())
+
+        # Init all Stations
+        self.stations = []
+        self.logger.write_info('Number of stations : ' + str(self.nb_stations))
+
+    def get_conf_dict(self):
+        confile = open(self.conf_file,'r')
+        conf_xml = confile.read()
+        confile.close()
+        return xmltodict(conf_xml,'utf-8')
+
+    def set_m3u_playlist(self):
+        m3u_dir = os.sep.join(self.m3u.split(os.sep)[:-1])
+        if not os.path.exists(m3u_dir):
+            os.makedirs(m3u_dir)
+        m3u = open(self.m3u, 'w')
+        m3u.write('#EXTM3U\n')
+        for s in self.stations:
+            info = '#EXTINF:%s,%s - %s\n' % ('-1',s.short_name, s.channel.name)
+            url =  s.channel.protocol + '://' + s.channel.host + ':' + str(s.channel.port) + s.channel.mount + '\n'
+            m3u.write(info)
+            m3u.write(url)
+        m3u.close()
+        self.logger.write_info('Writing M3U file to : ' + self.m3u)
+
+
+    def run(self):
+        q = Queue.Queue(1)
+
+        for i in range(0,self.nb_stations):
+            if isinstance(self.conf['deefuzzer']['station'], dict):
+                station = self.conf['deefuzzer']['station']
+            else:
+                station = self.conf['deefuzzer']['station'][i]
+            self.stations.append(Station(station, q, self.logger, self.m3u))
+
+        self.set_m3u_playlist()
+        p = Producer(q)
+        p.start()
+
+        # Start the Stations
+        for i in range(0,self.nb_stations):
+            self.stations[i].start()
+
+
+class Producer(Thread):
+    """a DeeFuzzer Producer master thread"""
+
+    def __init__(self, q):
+        Thread.__init__(self)
+        self.q = q
+
+    def run(self):
+        i=0
+        q = self.q
+        while True:
+            q.put(i,1)
+            i+=1
+
+
+def main():
+    if len(sys.argv) >= 2:
+        d = DeeFuzzer(sys.argv[-1])
+        d.start()
+    else:
+        text = prog_info()
+        sys.exit(text)
+
+if __name__ == '__main__':
+    main()
diff --git a/trunk/example/deefuzz_test.xml b/trunk/example/deefuzz_test.xml
new file mode 100644 (file)
index 0000000..5469e1b
--- /dev/null
@@ -0,0 +1,303 @@
+<deefuzzer>
+    <log>/var/log/deefuzzer/deefuzz_test.log</log>
+    <m3u>/var/www/m3u/deefuzz_test.m3u</m3u>
+
+    <station>
+        <infos>
+            <short_name>deefuzz_test</short_name>
+            <name>DeeFuzz Radio 100% Mix #TEST by http://deefuzz.parisson.com</name>
+            <description>DeeFuzz Radio 100% Mix #TEST - The Finest Techno House Electronic and Groovy Music Mixes fuzzed by YomGuy and JanoB (Cellar) from Paris 18e Chateau Rouge !</description>
+            <url>http://deefuzz.parisson.com</url>
+            <genre>House Techno Mix Mixed</genre>
+        </infos>
+        <server>
+            <host>parisson.com</host>
+            <port>8000</port>
+            <sourcepassword>source2parisson</sourcepassword>
+            <public>0</public>
+        </server>
+        <media>
+            <dir>/home/deefuzz/cellar/mix</dir>
+            <format>mp3</format>
+            <bitrate>192</bitrate>
+            <ogg_quality>7</ogg_quality>
+            <samplerate>44100</samplerate>
+            <voices>2</voices>
+            <shuffle>1</shuffle>
+        </media>
+        <rss>
+            <dir>/var/www/rss</dir>
+            <enclosure>1</enclosure>
+        </rss>
+        <twitter>
+            <mode>1</mode>
+            <user>deefuzz_test3</user>
+            <pass>z3fgvf12</pass>
+            <tags>house mix</tags>
+        </twitter>
+        <jingles>
+            <mode>1</mode>
+            <dir>/home/deefuzz/cellar/jingles</dir>
+            <shuffle>1</shuffle>
+        </jingles>
+        <control>
+            <mode>1</mode>
+            <port>1234</port>
+        </control>
+        <relay>
+            <mode>0</mode>
+            <url>http://parisson.com:8000/deefuzz_live.mp3</url>
+        </relay>
+    </station>
+
+        <station>
+        <infos>
+            <short_name>deefuzz_test2</short_name>
+            <name>DeeFuzz Radio 100% Mix #TEST by http://deefuzz.parisson.com</name>
+            <description>DeeFuzz Radio 100% Mix #TEST - The Finest Techno House Electronic and Groovy Music Mixes fuzzed by YomGuy and JanoB (Cellar) from Paris 18e Chateau Rouge !</description>
+            <url>http://deefuzz.parisson.com</url>
+            <genre>House Techno Mix Mixed</genre>
+        </infos>
+        <server>
+            <host>parisson.com</host>
+            <port>8000</port>
+            <sourcepassword>source2parisson</sourcepassword>
+            <public>0</public>
+        </server>
+        <media>
+            <dir>/home/deefuzz/cellar/mix</dir>
+            <format>mp3</format>
+            <bitrate>192</bitrate>
+            <ogg_quality>7</ogg_quality>
+            <samplerate>44100</samplerate>
+            <voices>2</voices>
+            <shuffle>1</shuffle>
+        </media>
+        <rss>
+            <dir>/var/www/rss</dir>
+            <enclosure>1</enclosure>
+        </rss>
+        <twitter>
+            <mode>0</mode>
+            <user>deefuzz_test3</user>
+            <pass>z3fgvf12</pass>
+            <tags>house mix</tags>
+        </twitter>
+        <jingles>
+            <mode>1</mode>
+            <dir>/home/deefuzz/cellar/jingles</dir>
+            <shuffle>1</shuffle>
+        </jingles>
+        <control>
+            <mode>0</mode>
+            <port>1234</port>
+        </control>
+        <relay>
+            <mode>0</mode>
+            <url>http://parisson.com:8000/deefuzz_live.mp3</url>
+        </relay>
+    </station>
+
+
+
+
+        <station>
+        <infos>
+            <short_name>deefuzz_test5</short_name>
+            <name>DeeFuzz Radio 100% Mix #TEST by http://deefuzz.parisson.com</name>
+            <description>DeeFuzz Radio 100% Mix #TEST - The Finest Techno House Electronic and Groovy Music Mixes fuzzed by YomGuy and JanoB (Cellar) from Paris 18e Chateau Rouge !</description>
+            <url>http://deefuzz.parisson.com</url>
+            <genre>House Techno Mix Mixed</genre>
+        </infos>
+        <server>
+            <host>localhost</host>
+            <port>8000</port>
+            <sourcepassword>source2parisson</sourcepassword>
+            <public>0</public>
+        </server>
+        <media>
+            <dir>/home/deefuzz/cellar/mix</dir>
+            <format>mp3</format>
+            <bitrate>192</bitrate>
+            <ogg_quality>7</ogg_quality>
+            <samplerate>44100</samplerate>
+            <voices>2</voices>
+            <shuffle>1</shuffle>
+        </media>
+        <rss>
+            <dir>/var/www/rss</dir>
+            <enclosure>1</enclosure>
+        </rss>
+        <twitter>
+            <mode>0</mode>
+            <user>deefuzz_test3</user>
+            <pass>z3fgvf12</pass>
+            <tags>house mix</tags>
+        </twitter>
+        <jingles>
+            <mode>1</mode>
+            <dir>/home/deefuzz/cellar/jingles</dir>
+            <shuffle>1</shuffle>
+        </jingles>
+        <control>
+            <mode>0</mode>
+            <port>1234</port>
+        </control>
+        <relay>
+            <mode>0</mode>
+            <url>http://parisson.com:8000/deefuzz_live.mp3</url>
+        </relay>
+    </station>
+
+
+
+        <station>
+        <infos>
+            <short_name>deefuzz_test6</short_name>
+            <name>DeeFuzz Radio 100% Mix #TEST by http://deefuzz.parisson.com</name>
+            <description>DeeFuzz Radio 100% Mix #TEST - The Finest Techno House Electronic and Groovy Music Mixes fuzzed by YomGuy and JanoB (Cellar) from Paris 18e Chateau Rouge !</description>
+            <url>http://deefuzz.parisson.com</url>
+            <genre>House Techno Mix Mixed</genre>
+        </infos>
+        <server>
+            <host>localhost</host>
+            <port>8000</port>
+            <sourcepassword>source2parisson</sourcepassword>
+            <public>0</public>
+        </server>
+        <media>
+            <dir>/home/deefuzz/cellar/mix</dir>
+            <format>mp3</format>
+            <bitrate>192</bitrate>
+            <ogg_quality>7</ogg_quality>
+            <samplerate>44100</samplerate>
+            <voices>2</voices>
+            <shuffle>1</shuffle>
+        </media>
+        <rss>
+            <dir>/var/www/rss</dir>
+            <enclosure>1</enclosure>
+        </rss>
+        <twitter>
+            <mode>1</mode>
+            <user>deefuzz_test3</user>
+            <pass>z3fgvf12</pass>
+            <tags>house mix</tags>
+        </twitter>
+        <jingles>
+            <mode>1</mode>
+            <dir>/home/deefuzz/cellar/jingles</dir>
+            <shuffle>1</shuffle>
+        </jingles>
+        <control>
+            <mode>0</mode>
+            <port>1234</port>
+        </control>
+        <relay>
+            <mode>0</mode>
+            <url>http://parisson.com:8000/deefuzz_live.mp3</url>
+        </relay>
+    </station>
+
+
+
+        <station>
+        <infos>
+            <short_name>deefuzz_test7</short_name>
+            <name>DeeFuzz Radio 100% Mix #TEST by http://deefuzz.parisson.com</name>
+            <description>DeeFuzz Radio 100% Mix #TEST - The Finest Techno House Electronic and Groovy Music Mixes fuzzed by YomGuy and JanoB (Cellar) from Paris 18e Chateau Rouge !</description>
+            <url>http://deefuzz.parisson.com</url>
+            <genre>House Techno Mix Mixed</genre>
+        </infos>
+        <server>
+            <host>localhost</host>
+            <port>8000</port>
+            <sourcepassword>source2parisson</sourcepassword>
+            <public>0</public>
+        </server>
+        <media>
+            <dir>/home/deefuzz/cellar/mix</dir>
+            <format>mp3</format>
+            <bitrate>192</bitrate>
+            <ogg_quality>7</ogg_quality>
+            <samplerate>44100</samplerate>
+            <voices>2</voices>
+            <shuffle>1</shuffle>
+        </media>
+        <rss>
+            <dir>/var/www/rss</dir>
+            <enclosure>1</enclosure>
+        </rss>
+        <twitter>
+            <mode>0</mode>
+            <user>deefuzz_test3</user>
+            <pass>z3fgvf12</pass>
+            <tags>house mix</tags>
+        </twitter>
+        <jingles>
+            <mode>1</mode>
+            <dir>/home/deefuzz/cellar/jingles</dir>
+            <shuffle>1</shuffle>
+        </jingles>
+        <control>
+            <mode>0</mode>
+            <port>1234</port>
+        </control>
+        <relay>
+            <mode>0</mode>
+            <url>http://parisson.com:8000/deefuzz_live.mp3</url>
+        </relay>
+    </station>
+
+
+
+            <station>
+        <infos>
+            <short_name>deefuzz_test8</short_name>
+            <name>DeeFuzz Radio 100% Mix #TEST by http://deefuzz.parisson.com</name>
+            <description>DeeFuzz Radio 100% Mix #TEST - The Finest Techno House Electronic and Groovy Music Mixes fuzzed by YomGuy and JanoB (Cellar) from Paris 18e Chateau Rouge !</description>
+            <url>http://deefuzz.parisson.com</url>
+            <genre>House Techno Mix Mixed</genre>
+        </infos>
+        <server>
+            <host>localhost</host>
+            <port>8000</port>
+            <sourcepassword>source2parisson</sourcepassword>
+            <public>0</public>
+        </server>
+        <media>
+            <dir>/home/deefuzz/cellar/mix</dir>
+            <format>mp3</format>
+            <bitrate>192</bitrate>
+            <ogg_quality>7</ogg_quality>
+            <samplerate>44100</samplerate>
+            <voices>2</voices>
+            <shuffle>1</shuffle>
+        </media>
+        <rss>
+            <dir>/var/www/rss</dir>
+            <enclosure>1</enclosure>
+        </rss>
+        <twitter>
+            <mode>0</mode>
+            <user>deefuzz_test3</user>
+            <pass>z3fgvf12</pass>
+            <tags>house mix</tags>
+        </twitter>
+        <jingles>
+            <mode>1</mode>
+            <dir>/home/deefuzz/cellar/jingles</dir>
+            <shuffle>1</shuffle>
+        </jingles>
+        <control>
+            <mode>0</mode>
+            <port>1234</port>
+        </control>
+        <relay>
+            <mode>0</mode>
+            <url>http://parisson.com:8000/deefuzz_live.mp3</url>
+        </relay>
+    </station>
+
+</deefuzzer>
+
diff --git a/trunk/example/deefuzz_xml_gen.py b/trunk/example/deefuzz_xml_gen.py
new file mode 100755 (executable)
index 0000000..26beb21
--- /dev/null
@@ -0,0 +1,25 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import sys
+from string import Template
+
+nb_station = sys.argv[1]
+deefuzz_from = sys.argv[2]
+deefuzz_to = sys.argv[3]
+
+df = open(deefuzz_from, 'r')
+dt = open(deefuzz_to, 'w')
+t = Template(df.read())
+
+dt.write('<deefuzz>\n')
+dt.write('    <log>/tmp/deefuzz.log</log>\n')
+dt.write('    <m3u>/tmp/deefuzz.m3u</m3u>\n')
+
+for i in range(0,int(nb_station)):
+    xml = t.substitute(number='_'+str(i+1))
+    dt.write(xml)
+dt.write('</deefuzz>\n')
+
+df.close()
+dt.close()
diff --git a/trunk/example/myfuzz.xml b/trunk/example/myfuzz.xml
new file mode 100644 (file)
index 0000000..872e5a7
--- /dev/null
@@ -0,0 +1,105 @@
+<deefuzzer>
+    <!-- A path to the log file (need write access rights to the directory) -->
+    <log>/path/to/log/mystation.log</log>
+    <!-- A path to the M3U playlist file (need write access rights to the directory).
+         The file is preferably accessible behind an url,
+         for example, http://mydomain.com/m3u/mystation.m3u -->
+    <m3u>/path/to/m3u/mystation.m3u</m3u>
+
+    <station>
+        <infos>
+            <!-- The short name of the station. It will define the mount point,
+                 for example http://mydomain.com:8000/my_station.mp3 -->
+            <short_name>my_station</short_name>
+            <!-- The name (title) of the station -->
+            <name>My best funky station</name>
+            <!-- The description of the station -->
+            <description>My personal best funky playlist ever !</description>
+            <!-- The website of the station (for metadata) -->
+            <url>http://mydomain.com</url>
+            <!-- The genres of the audio streams -->
+            <genre>Various Funk Groove</genre>
+        </infos>
+        <server>
+            <!-- The host to send the stream (domain or IP) -->
+            <host>mydomain.com</host>
+            <!-- The port of the Icecast2 server on the host -->
+            <port>8000</port>
+            <!-- The source password of the Icecast2 server on the host -->
+            <sourcepassword>icecast_source_password</sourcepassword>
+            <!-- If '1', the mount point will be publish on yp.icecast.org, '0' for private streams -->
+            <public>1</public>
+        </server>
+        <media>
+            <!-- The path to the directory containing all the media. It will be analyse recursively. -->
+            <dir>/path/to/mp3/</dir>
+            <!-- The audio format of the media. Can be 'mp3' or 'ogg' -->
+            <format>mp3</format>
+            <!-- The mean bitrate of the media -->
+            <bitrate>192</bitrate>
+            <!-- The ogg quality of the ogg vorbis media -->
+            <ogg_quality>7</ogg_quality>
+            <!-- The samplerate of the media -->
+            <samplerate>44100</samplerate>
+            <!-- The number of channels - or voices - of the media. '1' for mono, '2' for stereo. -->
+            <voices>2</voices>
+            <!-- If '1', the playlist will be randomized. '0' for aphanumeric order -->
+            <shuffle>1</shuffle>
+        </media>
+        <rss>
+            <!-- A path to the directory containing RSS (XML) files that is 'currently playing'
+                and 'playlist' feeds (need write access rights to the directory).
+                The file is preferably accessible behind an url,
+                for example, http://mydomain.com/rss/mystation.xml -->
+            <dir>/path/to/rss/</dir>
+            <!-- If '1', the RSS feeds will contain an enclosure to become a podcast feed.
+                '0' for simple RSS feed without enclosure -->
+            <enclosure>1</enclosure>
+        </rss>
+        <twitter>
+            <!-- If '1', deefuzzer will tweet #nowplaying, new tracks and other things to Twitter. 'O' does nothing. -->
+            <mode>0</mode>
+            <!-- Your twitter username -->
+            <user>my_twitter_user</user>
+            <!-- Your twitter password -->
+            <pass>my_twitter_password</pass>
+            <!-- These tags will be added to each twitter message -->
+            <tags>bla bla</tags>
+        </twitter>
+        <jingles>
+            <!-- If '1', some media will be played between each main track of the playlist. '0' does nothing. -->
+            <mode>0</mode>
+            <!-- A path to the directory containing jingles media files.
+                The files have to be of the same type of the main media files. -->
+            <dir>/path/to/jingles</dir>
+            <!-- If '1', the jingle playlist will be randomized. '0' for aphanumeric order -->
+            <shuffle>1</shuffle>
+        </jingles>
+        <control>
+            <!-- If '1', an OSC controller thread is started to allow external commands
+                See README for more info -->
+            <mode>0</mode>
+            <!-- The port of the OSC server -->
+            <port>1234</port>
+        </control>
+        <relay>
+            <!-- If '1', the station will relay another _existing_ stream.
+            Wonderful for commuting to live sessions. '0' does nothing -->
+            <mode>0</mode>
+            <!-- The URL of the station to relay -->
+            <url>http://anotherdomain.com:8000/stream.mp3</url>
+            <!-- The default author of the relay streams -->
+            <author>Me</author>
+        </relay>
+        <record>
+            <!-- If '1', the stream will be recorded, '0' does nothing -->
+            <mode>0</mode>
+            <!-- The directory where files will be recorded -->
+            <dir>/path/to/archives</dir>
+        </record>
+    </station>
+
+    <!-- Note that you can add many different stations in the same config file, thanks to the multi-threaded architecure ! -->
+</deefuzzer>
+
+
diff --git a/trunk/example/test.xml b/trunk/example/test.xml
new file mode 100644 (file)
index 0000000..e207c1a
--- /dev/null
@@ -0,0 +1,45 @@
+<deefuzzer>
+    <log>/tmp/deefuzz.log</log>
+    <m3u>/tmp/deefuzz.m3u</m3u>
+    <station>
+        <infos>
+            <short_name>My_Station_1</short_name>
+            <name>My best funky station</name>
+            <description>My personal best funky playlist ever !</description>
+            <url>http://mydomain.com</url>
+            <genre>Other</genre>
+            <channels>1</channels>
+        </infos>
+        <server>
+            <host>localhost</host>
+            <port>8000</port>
+            <sourcepassword>source2parisson</sourcepassword>
+            <public>1</public>
+        </server>
+        <media>
+            <dir>/home/momo/music/samples/th_drumloops_130bpm/mp3</dir>
+            <format>mp3</format>
+            <bitrate>192</bitrate>
+            <ogg_quality>7</ogg_quality>
+            <samplerate>44100</samplerate>
+            <voices>2</voices>
+            <shuffle>0</shuffle>
+        </media>
+        <rss>
+            <dir>/tmp/rss/</dir>
+            <enclosure>1</enclosure>
+        </rss>
+        <twitter>
+            <mode>1</mode>
+            <user>deefuzz_test</user>
+            <pass>badmomo</pass>
+            <tags>bla bla</tags>
+        </twitter>
+        <jingles>
+            <mode>1</mode>
+            <dir>/home/momo/music/samples/Bass/Electric Bass/mp3</dir>
+            <shuffle>1</shuffle>
+        </jingles>
+    </station>
+</deefuzzer>
+
diff --git a/trunk/example/yomguy_test.xml b/trunk/example/yomguy_test.xml
new file mode 100644 (file)
index 0000000..f248213
--- /dev/null
@@ -0,0 +1,108 @@
+<deefuzzer>
+    <log>/tmp/deefuzzer_test.log</log>
+    <m3u>/var/www/m3u/yomguy_test.m3u</m3u>
+        <station>
+        <infos>
+            <short_name>yomguy_diff</short_name>
+            <name>Yomguy playlist</name>
+            <description>Fine Funky House Music and other Various Grooves</description>
+            <url>http://yomix.org</url>
+            <genre>Various</genre>
+        </infos>
+        <server>
+            <host>parisson.com</host>
+            <port>8000</port>
+            <sourcepassword>source2parisson</sourcepassword>
+            <public>0</public>
+        </server>
+        <media>
+            <dir>/home/momo/music/mp3/cellar/mix</dir>
+            <format>mp3</format>
+            <bitrate>192</bitrate>
+            <ogg_quality>7</ogg_quality>
+            <samplerate>44100</samplerate>
+            <voices>2</voices>
+            <shuffle>1</shuffle>
+        </media>
+        <rss>
+            <dir>/tmp/rss/</dir>
+            <enclosure>1</enclosure>
+        </rss>
+        <twitter>
+            <mode>0</mode>
+            <user>deefuzz_test2</user>
+            <pass>badmomo</pass>
+            <tags>bla bla</tags>
+        </twitter>
+        <jingles>
+            <mode>0</mode>
+            <dir>/home/momo/music/samples/th_drumloops_130bpm/mp3</dir>
+            <shuffle>1</shuffle>
+        </jingles>
+        <control>
+            <mode>0</mode>
+            <port>1234</port>
+        </control>
+        <relay>
+            <mode>1</mode>
+            <url>http://127.0.0.1:8000/yomguy.mp3</url>
+            <author>YomGuy</author>
+        </relay>
+        <record>
+            <mode>0</mode>
+            <dir>/tmp</dir>
+        </record>
+    </station>
+    <station>
+        <infos>
+            <short_name>yomguy_rec</short_name>
+            <name>Yomguy playlist</name>
+            <description>Fine Funky House Music and other Various Grooves</description>
+            <url>http://yomix.org</url>
+            <genre>Various</genre>
+        </infos>
+        <server>
+            <host>127.0.0.1</host>
+            <port>8000</port>
+            <sourcepassword>source2parisson</sourcepassword>
+            <public>0</public>
+        </server>
+        <media>
+            <dir>/home/momo/music/mp3/cellar/mix</dir>
+            <format>mp3</format>
+            <bitrate>192</bitrate>
+            <ogg_quality>7</ogg_quality>
+            <samplerate>44100</samplerate>
+            <voices>2</voices>
+            <shuffle>1</shuffle>
+        </media>
+        <rss>
+            <dir>/tmp/rss/</dir>
+            <enclosure>1</enclosure>
+        </rss>
+        <twitter>
+            <mode>0</mode>
+            <user>deefuzz_test2</user>
+            <pass>badmomo</pass>
+            <tags>bla bla</tags>
+        </twitter>
+        <jingles>
+            <mode>0</mode>
+            <dir>/home/momo/music/samples/th_drumloops_130bpm/mp3</dir>
+            <shuffle>1</shuffle>
+        </jingles>
+        <control>
+            <mode>1</mode>
+            <port>1234</port>
+        </control>
+        <relay>
+            <mode>1</mode>
+            <url>http://127.0.0.1:8000/yomguy.mp3</url>
+            <author>YomGuy</author>
+        </relay>
+        <record>
+            <mode>1</mode>
+            <dir>/home/momo/tmp</dir>
+        </record>
+    </station>
+</deefuzzer>
diff --git a/trunk/example/yomguy_test1.xml b/trunk/example/yomguy_test1.xml
new file mode 100644 (file)
index 0000000..c88a2f2
--- /dev/null
@@ -0,0 +1,57 @@
+<deefuzzer>
+    <log>/tmp/deefuzzer.log</log>
+    <m3u>/var/www/m3u/yomguy.m3u</m3u>
+    <station>
+        <infos>
+            <short_name>yomguy</short_name>
+            <name>Yomguy's playlist</name>
+            <description>Fine Funky House Music and other Various Grooves</description>
+            <url>http://yomix.org</url>
+            <genre>Various</genre>
+        </infos>
+        <server>
+            <host>127.0.0.1</host>
+            <port>8000</port>
+            <sourcepassword>source2parisson</sourcepassword>
+            <public>0</public>
+        </server>
+        <media>
+            <dir>/home/momo/music/mp3/cellar/mix</dir>
+            <format>mp3</format>
+            <bitrate>192</bitrate>
+            <ogg_quality>7</ogg_quality>
+            <samplerate>44100</samplerate>
+            <voices>2</voices>
+            <shuffle>1</shuffle>
+        </media>
+        <rss>
+            <dir>/tmp/rss/</dir>
+            <enclosure>1</enclosure>
+        </rss>
+        <twitter>
+            <mode>0</mode>
+            <user>deefuzz_test2</user>
+            <pass>badmomo</pass>
+            <tags>bla bla</tags>
+        </twitter>
+        <jingles>
+            <mode>0</mode>
+            <dir>/home/momo/music/samples/th_drumloops_130bpm/mp3</dir>
+            <shuffle>1</shuffle>
+        </jingles>
+        <control>
+            <mode>1</mode>
+            <port>1234</port>
+        </control>
+        <relay>
+            <mode>0</mode>
+            <url>http://parisson.com:8000/deefuzz_1.mp3</url>
+            <author>YomGuy</author>
+        </relay>
+        <record>
+            <mode>0</mode>
+            <dir>/tmp</dir>
+        </record>
+    </station>
+</deefuzzer>
+
diff --git a/trunk/install.py b/trunk/install.py
new file mode 100644 (file)
index 0000000..4746d12
--- /dev/null
@@ -0,0 +1,72 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright Guillaume Pellerin (2006-2009)
+
+# <yomguy@parisson.com>
+
+# This software is a computer program whose purpose is to stream audio
+# and video data through icecast2 servers.
+
+# This software is governed by the CeCILL  license under French law and
+# abiding by the rules of distribution of free software.  You can  use,
+# modify and/ or redistribute the software under the terms of the CeCILL
+# license as circulated by CEA, CNRS and INRIA at the following URL
+# "http://www.cecill.info".
+
+# As a counterpart to the access to the source code and  rights to copy,
+# modify and redistribute granted by the license, users are provided only
+# with a limited warranty  and the software's author,  the holder of the
+# economic rights,  and the successive licensors  have only  limited
+# liability.
+
+# In this respect, the user's attention is drawn to the risks associated
+# with loading,  using,  modifying and/or developing or reproducing the
+# software by the user in light of its specific status of free software,
+# that may mean  that it is complicated to manipulate,  and  that  also
+# therefore means  that it is reserved for developers  and  experienced
+# professionals having in-depth computer knowledge. Users are therefore
+# encouraged to load and test the software's suitability as regards their
+# requirements in conditions enabling the security of their systems and/or
+# data to be ensured and,  more generally, to use and operate it in the
+# same conditions as regards security.
+
+# The fact that you are presently reading this means that you have had
+# knowledge of the CeCILL license and that you accept its terms.
+
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+# ONLY FOR LINUX / UNIX
+
+import os, sys
+import platform
+
+if len(sys.argv) == 1:
+    install_dir = '/usr/share/deefuzzer/'
+elif len(sys.argv) > 2:
+    sys.exit('Give just one directory to install the DeeFuzzer, or none.')
+else:
+    install_dir = sys.argv[1]
+
+if not os.path.exists(install_dir):
+    os.mkdir(install_dir)
+
+os.system('cp -ra ./* '+install_dir+os.sep)
+
+# Install shout-python
+os.chdir('shout-python')
+os.system('python setup.py install')
+os.chdir('..')
+
+os.system('easy_install tinyurl')
+
+if os.path.exists('/usr/bin/deefuzzer'):
+    os.system('rm -r /usr/bin/deefuzzer')
+
+os.system('ln -s '+install_dir+os.sep+'deefuzzer.py '+'/usr/bin/deefuzzer')
+
+print """
+   Installation successfull !
+   Type 'deefuzzer' now...
+   """
+
diff --git a/trunk/kill_deefuzzer.sh b/trunk/kill_deefuzzer.sh
new file mode 100755 (executable)
index 0000000..afc0582
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+kill -9 `pgrep deefuzzer`
+
diff --git a/trunk/shout-python/COPYING b/trunk/shout-python/COPYING
new file mode 100644 (file)
index 0000000..bf50f20
--- /dev/null
@@ -0,0 +1,482 @@
+                 GNU LIBRARY GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1991 Free Software Foundation, Inc.
+                   59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the library GPL.  It is
+ numbered 2 because it goes with version 2 of the ordinary GPL.]
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Library General Public License, applies to some
+specially designated Free Software Foundation software, and to any
+other libraries whose authors decide to use it.  You can use it for
+your libraries, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if
+you distribute copies of the library, or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link a program with the library, you must provide
+complete object files to the recipients so that they can relink them
+with the library, after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  Our method of protecting your rights has two steps: (1) copyright
+the library, and (2) offer you this license which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  Also, for each distributor's protection, we want to make certain
+that everyone understands that there is no warranty for this free
+library.  If the library is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original
+version, so that any problems introduced by others will not reflect on
+the original authors' reputations.
+\f
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that companies distributing free
+software will individually obtain patent licenses, thus in effect
+transforming the program into proprietary software.  To prevent this,
+we have made it clear that any patent must be licensed for everyone's
+free use or not licensed at all.
+
+  Most GNU software, including some libraries, is covered by the ordinary
+GNU General Public License, which was designed for utility programs.  This
+license, the GNU Library General Public License, applies to certain
+designated libraries.  This license is quite different from the ordinary
+one; be sure to read it in full, and don't assume that anything in it is
+the same as in the ordinary license.
+
+  The reason we have a separate public license for some libraries is that
+they blur the distinction we usually make between modifying or adding to a
+program and simply using it.  Linking a program with a library, without
+changing the library, is in some sense simply using the library, and is
+analogous to running a utility program or application program.  However, in
+a textual and legal sense, the linked executable is a combined work, a
+derivative of the original library, and the ordinary General Public License
+treats it as such.
+
+  Because of this blurred distinction, using the ordinary General
+Public License for libraries did not effectively promote software
+sharing, because most developers did not use the libraries.  We
+concluded that weaker conditions might promote sharing better.
+
+  However, unrestricted linking of non-free programs would deprive the
+users of those programs of all benefit from the free status of the
+libraries themselves.  This Library General Public License is intended to
+permit developers of non-free programs to use free libraries, while
+preserving your freedom as a user of such programs to change the free
+libraries that are incorporated in them.  (We have not seen how to achieve
+this as regards changes in header files, but we have achieved it as regards
+changes in the actual functions of the Library.)  The hope is that this
+will lead to faster development of free libraries.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, while the latter only
+works together with the library.
+
+  Note that it is possible for a library to be covered by the ordinary
+General Public License rather than by this special one.
+\f
+                 GNU LIBRARY GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library which
+contains a notice placed by the copyright holder or other authorized
+party saying it may be distributed under the terms of this Library
+General Public License (also called "this License").  Each licensee is
+addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+\f
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+\f
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+  6. As an exception to the Sections above, you may also compile or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    c) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    d) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the source code distributed need not include anything that is normally
+distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+\f
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Library General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                           NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library 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
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public
+    License along with this library; if not, write to the 
+    Free Software Foundation, Inc., 59 Temple Place - Suite 330, 
+    Boston, MA  02111-1307  USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/trunk/shout-python/INSTALL b/trunk/shout-python/INSTALL
new file mode 100644 (file)
index 0000000..5e4f1b6
--- /dev/null
@@ -0,0 +1,11 @@
+Prerequisites
+-------------
+
+You need to have libshout 2 installed. If you have pkg-config installed,
+make sure it can find shout (you may need to adjust PKG_CONFIG_PATH to
+contain $shout_prefix/lib/pkgcofig). Otherwise, shout-config must appear in
+your path.
+
+Installation
+------------
+run 'python setup.py install'
\ No newline at end of file
diff --git a/trunk/shout-python/MANIFEST b/trunk/shout-python/MANIFEST
new file mode 100644 (file)
index 0000000..530e5e9
--- /dev/null
@@ -0,0 +1,7 @@
+MANIFEST
+README
+INSTALL
+COPYING
+setup.py
+shout.c
+example.py
diff --git a/trunk/shout-python/README b/trunk/shout-python/README
new file mode 100644 (file)
index 0000000..399d78a
--- /dev/null
@@ -0,0 +1,7 @@
+This is shout-python, a set of bindings for libshout 2.
+
+shout-python allows you to act as a source for icecast 1 and 2, and
+shoutcast. This module includes inline documentation, or see
+the included example.py for a demonstration of its usage.
+
+shout-python is licensed under the GNU LGPL. See COPYING for details.
diff --git a/trunk/shout-python/example.py b/trunk/shout-python/example.py
new file mode 100755 (executable)
index 0000000..de4ce05
--- /dev/null
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+
+# usage: ./example.py /path/to/file1 /path/to/file2 ...
+import shout
+import sys
+import string
+import time
+
+s = shout.Shout()
+print "Using libshout version %s" % shout.version()
+
+# s.host = 'localhost'
+# s.port = 8000
+# s.user = 'source'
+s.password = 'hackme'
+s.mount = "/pyshout"
+# s.format = 'vorbis' | 'mp3'
+# s.protocol = 'http' | 'xaudiocast' | 'icy'
+# s.name = ''
+# s.genre = ''
+# s.url = ''
+# s.public = 0 | 1
+# s.audio_info = { 'key': 'val', ... }
+#  (keys are shout.SHOUT_AI_BITRATE, shout.SHOUT_AI_SAMPLERATE,
+#   shout.SHOUT_AI_CHANNELS, shout.SHOUT_AI_QUALITY)
+
+s.open()
+
+total = 0
+st = time.time()
+for fa in sys.argv[1:]:
+    print "opening file %s" % fa
+    f = open(fa)
+    s.set_metadata({'song': fa})
+
+    nbuf = f.read(4096)
+    while 1:
+        buf = nbuf
+        nbuf = f.read(4096)
+        total = total + len(buf)
+        if len(buf) == 0:
+            break
+        s.send(buf)
+        s.sync()
+    f.close()
+    
+    et = time.time()
+    br = total*0.008/(et-st)
+    print "Sent %d bytes in %d seconds (%f kbps)" % (total, et-st, br)
+
+print s.close()
diff --git a/trunk/shout-python/setup.py b/trunk/shout-python/setup.py
new file mode 100644 (file)
index 0000000..c33ff4c
--- /dev/null
@@ -0,0 +1,61 @@
+# distutils build script
+# To install shout-python, run 'python setup.py install'
+
+from distutils.core import setup, Extension
+import os
+import sys
+
+ver = '0.2'
+
+# write default shout.pc path into environment if PKG_CONFIG_PATH is unset
+if not os.environ.has_key('PKG_CONFIG_PATH'):
+  os.environ['PKG_CONFIG_PATH'] = '/usr/local/lib/pkgconfig'
+
+# Find shout compiler/linker flag via pkgconfig or shout-config
+if os.system('pkg-config --exists shout 2> /dev/null') == 0:
+  pkgcfg = os.popen('pkg-config --cflags shout')
+  cflags = pkgcfg.readline().strip()
+  pkgcfg.close()
+  pkgcfg = os.popen('pkg-config --libs shout')
+  libs = pkgcfg.readline().strip()
+  pkgcfg.close()
+
+else:
+  if os.system('pkg-config --usage 2> /dev/null') == 0:
+    print "pkg-config could not find libshout: check PKG_CONFIG_PATH"
+  if os.system('shout-config 2> /dev/null') == 0:
+    scfg = os.popen('shout-config --cflags')
+    cflags = scfg.readline().strip()
+    scfg.close()
+    scfg = os.popen('shout-config --libs')
+    libs = scfg.readline().strip()
+    scfg.close()
+
+  else:
+    print "pkg-config and shout-config unavailable, build terminated"
+    sys.exit(1)
+
+# there must be an easier way to set up these flags!
+iflags = [x[2:] for x in cflags.split() if x[0:2] == '-I']
+extra_cflags = [x for x in cflags.split() if x[0:2] != '-I']
+libdirs = [x[2:] for x in libs.split() if x[0:2] == '-L']
+libsonly = [x[2:] for x in libs.split() if x[0:2] == '-l']
+
+# include_dirs=[]
+# libraries=[]
+# runtime_library_dirs=[]
+# extra_objects, extra_compile_args, extra_link_args
+shout = Extension('shout', sources = ['shout.c'],
+                  include_dirs = iflags,
+                  extra_compile_args = extra_cflags,
+                  library_dirs = libdirs,
+                  libraries = libsonly)
+
+# data_files = []
+setup (name = 'shout-python',
+       version = ver,
+       description = 'Bindings for libshout 2',
+       url = 'http://icecast.org/download.php',
+       author = 'Brendan Cully',
+       author_email = 'brendan@xiph.org',
+       ext_modules = [shout])
diff --git a/trunk/shout-python/shout.c b/trunk/shout-python/shout.c
new file mode 100644 (file)
index 0000000..1e6fe72
--- /dev/null
@@ -0,0 +1,606 @@
+/* shout.c: python bindings to libshout
+ * Copyright (c) 2003,5 Brendan Cully <brendan@xiph.org>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Library General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2 of the License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Library General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public
+ *  License along with this library; if not, write to the 
+ *  Free Software Foundation, Inc., 59 Temple Place - Suite 330, 
+ *  Boston, MA  02111-1307  USA.
+ *
+ * $Id$
+ */
+
+#include <Python.h>
+#include <shout/shout.h>
+
+static PyObject* ShoutError;
+
+typedef struct {
+  PyObject_HEAD
+  shout_t* conn;
+  PyObject* attr;
+} ShoutObject;
+
+typedef int(*pshout_set_shout)(shout_t*);
+typedef int(*pshout_set_shout_int)(shout_t*, int);
+typedef int(*pshout_set_shout_str)(shout_t*, const char*);
+
+typedef struct _ShoutObjectAttr {
+  const char* name;
+  pshout_set_shout set_shout;
+  int(*set)(struct _ShoutObjectAttr*, ShoutObject*, PyObject*);
+} ShoutObjectAttr;
+
+typedef struct {
+  const char* name;
+  int val;
+} kv_strint;
+
+/* -- module prototypes -- */
+static PyObject* pshout_version(PyObject* self, PyObject* args);
+
+/* -- ShoutObject instance prototypes -- */
+static PyObject* pshoutobj_new(PyObject* self, PyObject* args);
+static void pshoutobj_initattrs(PyObject* self);
+static void pshoutobj_free(PyObject* self);
+static PyObject* pshoutobj_getattr(PyObject* self, char* name);
+static int pshoutobj_setattr(PyObject* self, char* name, PyObject* v);
+
+static PyObject* pshoutobj_open(ShoutObject* self);
+static PyObject* pshoutobj_close(ShoutObject* self);
+static PyObject* pshoutobj_get_connected(ShoutObject* self);
+static PyObject* pshoutobj_send(ShoutObject* self, PyObject* args);
+static PyObject* pshoutobj_sync(ShoutObject* self);
+static PyObject* pshoutobj_delay(ShoutObject* self);
+static PyObject* pshoutobj_queuelen(ShoutObject* self);
+static PyObject* pshoutobj_set_metadata(ShoutObject* self, PyObject* args);
+
+/* -- attr prototypes -- */
+static int pshoutobj_set_str(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v);
+static int pshoutobj_set_int(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v);
+static int pshoutobj_set_bool(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v);
+static int pshoutobj_set_proto(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v);
+static int pshoutobj_set_fmt(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v);
+static int pshoutobj_set_audio_info(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v);
+
+static char docstring[] = "Shout library v2 interface\n\n"
+  "Use this module to send audio data to an icecast (or shoutcast) server\n"
+  "shout.Shout() creates a new Shout object.\n\n"
+
+  "Shout\n\n"
+  "Use this object to send data to an icecast server.\n"
+  "Set the connection attributes before calling \"open\" (at least\n"
+  "\"host\", \"port\", \"password\" and \"mount\" must be specified).\n\n"
+  "Methods:\n"
+  "            open() - connect to server\n"
+  "   get_connected() - monitor connection status in nonblocking mode\n"
+  "           close() - disconnect from server\n"
+  "        send(data) - send audio data to server\n"
+  "            sync() - sleep until server needs more data. This is equal to\n"
+  "                     the time it takes to play data sent since last sync\n"
+  "           delay() - return milliseconds to wait before sending more data\n"
+  "        queuelen() - return number of bytes on the nonblocking write queue\n"
+  "set_metadata(dict) - update stream metadata on server (current known key is\n"
+  "                     \"song\". Not currently supported for ogg.\n\n"
+  "Attributes:\n"
+  "       host - name or address of destination server\n"
+  "       port - port of destination server\n"
+  "       user - source user name (optional)\n"
+  "   password - source password\n"
+  "      mount - mount point on server (relative URL, eg \"/stream.ogg\")\n"
+  "   protocol - server protocol: \"http\" (the default) for icecast 2,\n"
+  "              \"xaudiocast\" for icecast 1, or \"icy\" for shoutcast\n"
+  "nonblocking - use nonblocking send\n"
+  "     format - audio format: \"ogg\" (the default) or \"mp3\"\n"
+  "       name - stream name\n"
+  "        url - stream web page\n"
+  "      genre - stream genre\n"
+  "description - longer stream description\n"
+  " audio_info - dictionary of stream audio parameters, for YP information.\n"
+  "              Useful keys include \"bitrate\" (in kbps), \"samplerate\"\n"
+  "              (in Hz), \"channels\" and \"quality\" (Ogg encoding\n"
+  "              quality). All dictionary values should be strings. The known\n"
+  "              keys are defined as the SHOUT_AI_* constants, but any other\n"
+  "              will be passed along to the server as well.\n"
+  "   dumpfile - file name to record stream to on server (not supported on\n"
+  "              all servers)\n"
+  "      agent - for customizing the HTTP user-agent header\n\n";
+
+static PyTypeObject ShoutObject_Type = {
+  PyObject_HEAD_INIT(NULL)
+  0,
+  "shout.Shout",
+  sizeof(ShoutObject),
+  0,
+  pshoutobj_free, /* tp_dealloc */
+  0,           /* tp_print */
+  pshoutobj_getattr,
+  pshoutobj_setattr,
+  0,           /* tp_compare */
+  0,           /* tp_repr */
+  0,           /* tp_as_number */
+  0,           /* tp_as_sequence */
+  0,           /* tp_as_mapping */
+  0,           /* tp_hash */
+  0,           /* tp_call */
+  0,           /* tp_str */
+  0,           /* tp_getattro */
+  0,           /* tp_setattro */
+  0,           /* tp_as_buffer */
+  0,           /* tp_flags */
+  "See shout module help: help(shout)\n"
+};
+
+static PyMethodDef ShoutMethods[] = {
+  { "version", pshout_version, METH_VARARGS,
+    "Return the version of libshout being used, as a string." },
+  { "Shout", pshoutobj_new, METH_VARARGS,
+    "Create a new Shout object." },
+  { NULL, NULL, 0, NULL }
+};
+
+static ShoutObjectAttr ShoutObjectAttrs[] = {
+  { "host",        (pshout_set_shout)shout_set_host, pshoutobj_set_str },
+  { "port",        (pshout_set_shout)shout_set_port, pshoutobj_set_int },
+  { "user",        (pshout_set_shout)shout_set_user, pshoutobj_set_str },
+  { "password",    (pshout_set_shout)shout_set_password, pshoutobj_set_str },
+  { "agent",       (pshout_set_shout)shout_set_agent, pshoutobj_set_str },
+  { "format",      (pshout_set_shout)shout_set_format, pshoutobj_set_fmt },
+  { "protocol",    (pshout_set_shout)shout_set_protocol, pshoutobj_set_proto },
+  { "nonblocking", (pshout_set_shout)shout_set_nonblocking, pshoutobj_set_bool },
+  { "mount",       (pshout_set_shout)shout_set_mount, pshoutobj_set_str },
+  { "name",        (pshout_set_shout)shout_set_name, pshoutobj_set_str },
+  { "url",         (pshout_set_shout)shout_set_url, pshoutobj_set_str },
+  { "genre",       (pshout_set_shout)shout_set_genre, pshoutobj_set_str },
+  { "description", (pshout_set_shout)shout_set_description, pshoutobj_set_str },
+  { "public",      (pshout_set_shout)shout_set_public, pshoutobj_set_int },
+  { "dumpfile",    (pshout_set_shout)shout_set_dumpfile, pshoutobj_set_str },
+  { "audio_info",  NULL, pshoutobj_set_audio_info },
+  { NULL, NULL, NULL }
+};
+
+static kv_strint ShoutProtocolMap[] = {
+  { "http",       SHOUT_PROTOCOL_HTTP },
+  { "xaudiocast", SHOUT_PROTOCOL_XAUDIOCAST },
+  { "icy",        SHOUT_PROTOCOL_ICY },
+  { NULL, 0 }
+};
+
+static kv_strint ShoutFormatMap[] = {
+  { "ogg",    SHOUT_FORMAT_OGG },
+  { "mp3",    SHOUT_FORMAT_MP3 },
+  { "vorbis", SHOUT_FORMAT_OGG }, /* for backwards compatability */
+  { NULL, 0 }
+};
+
+static PyMethodDef ShoutObjectMethods[] = {
+  { "open", (PyCFunction)pshoutobj_open, METH_NOARGS,
+    "Connect to server." },
+  { "close", (PyCFunction)pshoutobj_close, METH_NOARGS,
+    "Close connection to server." },
+  { "get_connected", (PyCFunction)pshoutobj_get_connected, METH_NOARGS,
+    "Check for connection progress." },
+  { "send", (PyCFunction)pshoutobj_send, METH_VARARGS,
+    "Send audio data to server." },
+  { "sync", (PyCFunction)pshoutobj_sync, METH_NOARGS,
+    "Sleep for time required to play previously sent audio data." },
+  { "delay", (PyCFunction)pshoutobj_delay, METH_NOARGS,
+    "Return amount of time in milliseconds to wait before sending more data." },
+  { "queuelen", (PyCFunction)pshoutobj_queuelen, METH_NOARGS,
+    "Return the number of bytes currently on the write queue for nonblocking send" },
+  { "set_metadata", (PyCFunction)pshoutobj_set_metadata, METH_VARARGS,
+    "Update stream metadata on server (takes a dictionary argument. Current keys are: \"song\"" },
+
+  /* attributes (cribbed from Arc's ogg-python technique) */
+  { "host", NULL, 0, NULL },
+  { "port", NULL, 0, NULL },
+  { "user", NULL, 0, NULL },
+  { "password", NULL, 0, NULL },
+  { "agent", NULL, 0, NULL },
+  { "format", NULL, 0, NULL },
+  { "protocol", NULL, 0, NULL },
+  { "nonblocking", NULL, 0, NULL },
+  { "mount", NULL, 0, NULL },
+  { "name", NULL, 0, NULL },
+  { "url", NULL, 0, NULL },
+  { "genre", NULL, 0, NULL },
+  { "description", NULL, 0, NULL },
+  { "public", NULL, 0, NULL },
+  { "dumpfile", NULL, 0, NULL },
+  { "audio_info", NULL, 0, NULL },
+  
+  /* sentinel */
+  { NULL, NULL, 0, NULL }
+};
+
+void initshout(void) {
+  PyObject* mod;
+  PyObject* dict;
+
+  ShoutObject_Type.ob_type = &PyType_Type;
+
+  mod = Py_InitModule3("shout", ShoutMethods, docstring);
+  dict = PyModule_GetDict(mod);
+  ShoutError = PyErr_NewException("shout.ShoutException", NULL, NULL);
+  PyDict_SetItemString(dict, "ShoutException", ShoutError);
+
+  PyModule_AddIntConstant(mod, "SHOUTERR_SUCCESS", SHOUTERR_SUCCESS);
+  PyModule_AddIntConstant(mod, "SHOUTERR_INSANE", SHOUTERR_INSANE);
+  PyModule_AddIntConstant(mod, "SHOUTERR_NOCONNECT", SHOUTERR_NOCONNECT);
+  PyModule_AddIntConstant(mod, "SHOUTERR_NOLOGIN", SHOUTERR_NOLOGIN);
+  PyModule_AddIntConstant(mod, "SHOUTERR_SOCKET", SHOUTERR_SOCKET);
+  PyModule_AddIntConstant(mod, "SHOUTERR_MALLOC", SHOUTERR_MALLOC);
+  PyModule_AddIntConstant(mod, "SHOUTERR_METADATA", SHOUTERR_METADATA);
+  PyModule_AddIntConstant(mod, "SHOUTERR_CONNECTED", SHOUTERR_CONNECTED);
+  PyModule_AddIntConstant(mod, "SHOUTERR_UNCONNECTED", SHOUTERR_UNCONNECTED);
+  PyModule_AddIntConstant(mod, "SHOUTERR_UNSUPPORTED", SHOUTERR_UNSUPPORTED);
+  PyModule_AddIntConstant(mod, "SHOUTERR_BUSY", SHOUTERR_BUSY);
+
+  PyModule_AddStringConstant(mod, "SHOUT_AI_BITRATE", SHOUT_AI_BITRATE);
+  PyModule_AddStringConstant(mod, "SHOUT_AI_SAMPLERATE", SHOUT_AI_SAMPLERATE);
+  PyModule_AddStringConstant(mod, "SHOUT_AI_CHANNELS", SHOUT_AI_CHANNELS);
+  PyModule_AddStringConstant(mod, "SHOUT_AI_QUALITY", SHOUT_AI_QUALITY);
+}
+
+/* -- shout module methods -- */
+
+static PyObject* pshout_version(PyObject* self, PyObject* args) {
+  if (!PyArg_ParseTuple(args, ""))
+    return NULL;
+
+  return Py_BuildValue("s", shout_version(NULL, NULL, NULL));
+}
+
+/* -- ShoutObject instance methods -- */
+
+static PyObject* pshoutobj_new(PyObject* self, PyObject* args) {
+  ShoutObject* me;
+
+  if (!PyArg_ParseTuple(args, ""))
+    return NULL;
+
+  if (!(me = PyObject_New(ShoutObject, &ShoutObject_Type)))
+    return NULL;
+
+  me->attr = NULL;
+
+  if (!(me->conn = shout_new())) {
+    PyErr_NoMemory();
+    PyObject_Del(self);
+
+    return NULL;
+  }
+
+  return (PyObject*)me;
+}
+
+static void pshoutobj_free(PyObject* self) {
+  ShoutObject* me = (ShoutObject*)self;
+
+  Py_XDECREF(me->attr);
+  shout_free(me->conn);
+  PyObject_Del(self);
+}
+
+static void pshoutobj_initattrs(PyObject* self) {
+  shout_t* conn = ((ShoutObject*)self)->conn;
+  int val, i;
+
+  pshoutobj_setattr(self, "host", Py_BuildValue("s", shout_get_host(conn)));
+  pshoutobj_setattr(self, "port", Py_BuildValue("i", shout_get_port(conn)));
+  pshoutobj_setattr(self, "user", Py_BuildValue("s", shout_get_user(conn)));
+  pshoutobj_setattr(self, "password", Py_BuildValue(""));
+  pshoutobj_setattr(self, "mount", Py_BuildValue(""));
+  pshoutobj_setattr(self, "name", Py_BuildValue(""));
+  pshoutobj_setattr(self, "url", Py_BuildValue(""));
+  pshoutobj_setattr(self, "genre", Py_BuildValue(""));
+  pshoutobj_setattr(self, "description", Py_BuildValue(""));
+  pshoutobj_setattr(self, "audio_info", Py_BuildValue(""));
+  pshoutobj_setattr(self, "dumpfile", Py_BuildValue(""));  
+  pshoutobj_setattr(self, "agent", Py_BuildValue("s", shout_get_agent(conn)));
+  pshoutobj_setattr(self, "protocol", Py_BuildValue(""));
+  pshoutobj_setattr(self, "nonblocking", shout_get_nonblocking(conn) ? Py_True : Py_False);
+  pshoutobj_setattr(self, "format", Py_BuildValue(""));
+
+  val = shout_get_protocol(conn);
+  for (i = 0; ShoutProtocolMap[i].name; i++)
+    if (ShoutProtocolMap[i].val == val) {
+      pshoutobj_setattr(self, "protocol", Py_BuildValue("s", ShoutProtocolMap[i].name));
+      break;
+    }
+
+  val = shout_get_format(conn);
+  for (i = 0; ShoutFormatMap[i].name; i++)
+    if (ShoutFormatMap[i].val == val) {
+      pshoutobj_setattr(self, "format", Py_BuildValue("s", ShoutFormatMap[i].name));
+      break;
+    }
+}
+
+static PyObject* pshoutobj_getattr(PyObject* self, char* name) {
+  ShoutObject* me = (ShoutObject*)self;
+
+  if (!me->attr)
+    pshoutobj_initattrs(self);
+
+  if (me->attr) {
+    PyObject* v = PyDict_GetItemString(me->attr, name);
+    if (v) {
+      Py_INCREF(v);
+      return v;
+    }
+  }
+  return Py_FindMethod(ShoutObjectMethods, self, name);
+}
+
+static int pshoutobj_setattr(PyObject* self, char* name, PyObject* v) {
+  ShoutObject* me = (ShoutObject*)self;
+  ShoutObjectAttr* attr;
+
+  if (!me->attr && !(me->attr = PyDict_New()))
+    return -1;
+
+  if (v == NULL)
+    return -1;
+  
+  for (attr = ShoutObjectAttrs; attr->name; attr++) {
+    if (!strcmp(attr->name, name)) {
+      if (v != Py_None && attr->set(attr, me, v) != SHOUTERR_SUCCESS) {
+       if (!PyErr_Occurred())
+         PyErr_SetString(ShoutError, shout_get_error(me->conn));
+        return -1;
+      }
+      break;
+    }
+  }
+
+  return PyDict_SetItemString(me->attr, name, v);
+}
+
+static PyObject* pshoutobj_open(ShoutObject* self) {
+  int ret;
+  Py_BEGIN_ALLOW_THREADS
+  ret=shout_open(self->conn);
+  Py_END_ALLOW_THREADS
+  if (!((ret == SHOUTERR_SUCCESS)||
+        ((ret==SHOUTERR_BUSY) && shout_get_nonblocking(self->conn)))) {
+    PyErr_SetString(ShoutError, shout_get_error(self->conn));
+    
+    return NULL;
+  }
+
+  return Py_BuildValue("i", 1);
+}
+
+static PyObject* pshoutobj_close(ShoutObject* self) {
+  if (shout_close(self->conn) != SHOUTERR_SUCCESS) {
+    PyErr_SetString(ShoutError, shout_get_error(self->conn));
+
+    return NULL;
+  }
+
+  return Py_BuildValue("i", 1);
+}
+
+static PyObject* pshoutobj_send(ShoutObject* self, PyObject* args) {
+  const unsigned char* data;
+  size_t len;
+  int res;
+
+  if (!PyArg_ParseTuple(args, "s#", &data, &len))
+    return NULL;
+
+  Py_BEGIN_ALLOW_THREADS
+  res = shout_send(self->conn, data, len);
+  Py_END_ALLOW_THREADS
+
+  if (res != SHOUTERR_SUCCESS) { 
+    PyErr_SetString(ShoutError, shout_get_error(self->conn));
+
+    return NULL;
+  }
+
+  return Py_BuildValue("i", 1);
+}
+
+static PyObject* pshoutobj_sync(ShoutObject* self) {
+  Py_BEGIN_ALLOW_THREADS
+  shout_sync(self->conn);
+  Py_END_ALLOW_THREADS
+
+  return Py_BuildValue("i", 1);
+}
+
+static PyObject* pshoutobj_get_connected(ShoutObject* self) {
+  return Py_BuildValue("i", shout_get_connected(self->conn));
+}
+
+static PyObject* pshoutobj_delay(ShoutObject* self) {
+  return Py_BuildValue("i", shout_delay(self->conn));
+}
+
+static PyObject* pshoutobj_queuelen(ShoutObject* self) {
+  return Py_BuildValue("i", shout_queuelen(self->conn));
+}
+
+static PyObject* pshoutobj_set_metadata(ShoutObject* self, PyObject* args) {
+  shout_metadata_t* metadata;
+  PyObject* dict;
+  PyObject* key;
+  PyObject* val;
+  const char* skey;
+  const char* sval;
+  Py_ssize_t i = 0;
+  int rc;
+
+  if (!(metadata = shout_metadata_new())) {
+    PyErr_NoMemory();
+    return NULL;
+  }
+
+  if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &dict))
+    return NULL;
+
+  while (PyDict_Next(dict, &i, &key, &val)) {
+    if (!PyString_Check(key)) {
+      PyErr_SetString(PyExc_TypeError, "Dictionary key must be string");
+      shout_metadata_free(metadata);
+      return NULL;
+    }
+    if (!PyString_Check(val)) {
+      PyErr_SetString(PyExc_TypeError, "Dictionary value must be string");
+      shout_metadata_free(metadata);
+      return NULL;
+    }
+
+    skey = PyString_AsString(key);
+    sval = PyString_AsString(val);
+
+    if ((rc = shout_metadata_add(metadata, skey, sval)) != SHOUTERR_SUCCESS) {
+      if (rc == SHOUTERR_MALLOC)
+       PyErr_NoMemory();
+      else if (rc == SHOUTERR_INSANE)
+       PyErr_SetString(PyExc_TypeError, "Dictionary key must not be empty");
+      shout_metadata_free(metadata);
+      return NULL;
+    }
+  }
+
+  rc = shout_set_metadata(self->conn, metadata);
+  shout_metadata_free(metadata);
+
+  if (rc != SHOUTERR_SUCCESS) {
+    PyErr_SetString(ShoutError, "Metadata not supported in this connection");
+    return NULL;
+  }
+
+  return Py_BuildValue("i", 1);
+}
+
+static int pshoutobj_set_str(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v) {
+  const char* str;
+  pshout_set_shout_str set_shout;
+
+  if (!PyString_Check(v)) {
+    PyErr_SetString(PyExc_TypeError, "String argument required");
+    return -1;
+  }
+
+  str = PyString_AsString(v);
+  set_shout = (pshout_set_shout_str)attr->set_shout;
+  return set_shout(self->conn, str);
+}
+
+static int pshoutobj_set_int(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v) {
+  long val;
+  pshout_set_shout_int set_shout;
+
+  if (!PyInt_Check(v)) {
+    PyErr_SetString(PyExc_TypeError, "Numerical argument required");
+    return -1;
+  }
+  
+  val = PyLong_AsLong(v);
+  set_shout = (pshout_set_shout_int)attr->set_shout;
+  return set_shout(self->conn, val);
+}
+
+static int pshoutobj_set_bool(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v) {
+  long val;
+  pshout_set_shout_int set_shout;
+
+  if (!PyBool_Check(v)) {
+    PyErr_SetString(PyExc_TypeError, "Boolean argument required");
+    return -1;
+  }
+
+  val = (v == Py_True) ? 1 : 0;
+  set_shout = (pshout_set_shout_int)attr->set_shout;
+  return set_shout(self->conn, val);
+}
+
+static int pshoutobj_set_fmt(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v) {
+  const char* val;
+  kv_strint* fmt_map;
+  pshout_set_shout_int set_shout;
+
+  if (!PyString_Check(v)) {
+    PyErr_SetString(PyExc_TypeError, "String argument required");
+    return SHOUTERR_INSANE;
+  }
+
+  val = PyString_AsString(v);
+  for (fmt_map = ShoutFormatMap; fmt_map->name; fmt_map++) {
+    if (!strcmp(fmt_map->name, val)) {
+      set_shout = (pshout_set_shout_int)attr->set_shout;
+      return set_shout(self->conn, fmt_map->val);
+    }
+  }
+
+  PyErr_SetString(ShoutError, "Unsupported format");
+  return SHOUTERR_UNSUPPORTED;
+}
+
+static int pshoutobj_set_proto(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v) {
+  const char* val;
+  kv_strint* proto_map;
+  pshout_set_shout_int set_shout;
+
+  if (!PyString_Check(v)) {
+    PyErr_SetString(PyExc_TypeError, "String argument required");
+    return SHOUTERR_INSANE;
+  }
+
+  val = PyString_AsString(v);
+  for (proto_map = ShoutProtocolMap; proto_map->name; proto_map++) {
+    if (!strcmp(proto_map->name, val)) {
+      set_shout = (pshout_set_shout_int)attr->set_shout;
+      return set_shout(self->conn, proto_map->val);
+    }
+  }
+
+  PyErr_SetString(ShoutError, "Unsupported protocol");
+  return SHOUTERR_UNSUPPORTED;
+}
+
+static int pshoutobj_set_audio_info(ShoutObjectAttr* attr, ShoutObject* self, PyObject* v) {
+  PyObject* key;
+  PyObject* val;
+  const char* skey;
+  const char* sval;
+  Py_ssize_t i = 0;
+  int rc;
+
+  if (!PyDict_Check(v)) {
+    PyErr_SetString(PyExc_TypeError, "Dictionary argument required");
+    return SHOUTERR_INSANE;
+  }
+
+  while (PyDict_Next(v, &i, &key, &val)) {
+    if (!PyString_Check(key)) {
+      PyErr_SetString(PyExc_TypeError, "Dictionary key must be string");
+      return SHOUTERR_INSANE;
+    }
+    if (!PyString_Check(val)) {
+      PyErr_SetString(PyExc_TypeError, "Dictionary value must be string");
+      return SHOUTERR_INSANE;
+    }
+
+    skey = PyString_AsString(key);
+    sval = PyString_AsString(val);
+
+    if ((rc = shout_set_audio_info(self->conn, skey, sval)) != SHOUTERR_SUCCESS)
+      return rc;
+  }
+
+  return SHOUTERR_SUCCESS;
+}
diff --git a/trunk/tests/test_twitter2.py b/trunk/tests/test_twitter2.py
new file mode 100644 (file)
index 0000000..ee4fd01
--- /dev/null
@@ -0,0 +1,50 @@
+#!/usr/bin/python2.6
+# -*- coding: utf-8 -*-
+
+import twitter
+
+# DeeFuzzer consumer keys
+
+consumer_key    = 'ozs9cPS2ci6eYQzzMSTb4g'
+consumer_secret = '1kNEffHgGSXO2gMNTr8HRum5s2ofx3VQnJyfd0es'
+
+# Token keys
+
+# YomGuy
+#access_token_key = '18295650-30TgtgZTrWuoS0lkYx4W2W3mDKbc1wkl85Q70fRUo'
+#access_token_secret = 'lNArajV6ENBHIsDod9EPxupkRnlxBCPJxrPOpFPR8'
+
+# DeeFuzz Radio
+access_token_key = '76728330-6g3ISHcbGiXDICRdYCOmQtfZioZ2M8gsHH9HqALXd'
+access_token_secret = 'YHx0Dc7gpCgkVUouURdByZ43ZNStCBA1ZUhKJUklo'
+
+
+api = twitter.Api(username=consumer_key,
+                  password=consumer_secret,
+                  access_token_key=access_token_key,
+                  access_token_secret=access_token_secret)
+
+#print 'Friends :'
+#friends = api.GetFriends()
+#print [u.name for u in friends]
+
+#followers = []
+#for page in range(0,2):
+    #try:
+        #f = api.GetFollowers(page=page)
+        #for follower in f:
+            #followers.append(follower)
+    #except:
+        #raise 'No page :', page
+
+#print 'Followers :'
+#print [u.name for u in followers]
+#print 'Length : ', len(followers)
+
+#print 'Mentions :'
+#mentions = api.GetMentions()
+#print [(m.user.name, m.text) for m in mentions]
+
+print 'Statuses :'
+statuses = api.GetUserTimeline('deefuzz_radio')
+print [(s.user.id, s.user.name, s.text, s.created_at) for s in statuses]
diff --git a/trunk/tools/PyRSS2Gen.py b/trunk/tools/PyRSS2Gen.py
new file mode 100644 (file)
index 0000000..fc1f1cf
--- /dev/null
@@ -0,0 +1,443 @@
+"""PyRSS2Gen - A Python library for generating RSS 2.0 feeds."""
+
+__name__ = "PyRSS2Gen"
+__version__ = (1, 0, 0)
+__author__ = "Andrew Dalke <dalke@dalkescientific.com>"
+
+_generator_name = __name__ + "-" + ".".join(map(str, __version__))
+
+import datetime
+
+# Could make this the base class; will need to add 'publish'
+class WriteXmlMixin:
+    def write_xml(self, outfile, encoding = "iso-8859-1"):
+        from xml.sax import saxutils
+        handler = saxutils.XMLGenerator(outfile, encoding)
+        handler.startDocument()
+        self.publish(handler)
+        handler.endDocument()
+
+    def to_xml(self, encoding = "iso-8859-1"):
+        try:
+            import cStringIO as StringIO
+        except ImportError:
+            import StringIO
+        f = StringIO.StringIO()
+        self.write_xml(f, encoding)
+        return f.getvalue()
+
+
+def _element(handler, name, obj, d = {}):
+    if isinstance(obj, basestring) or obj is None:
+        # special-case handling to make the API easier
+        # to use for the common case.
+        handler.startElement(name, d)
+        if obj is not None:
+            handler.characters(obj)
+        handler.endElement(name)
+    else:
+        # It better know how to emit the correct XML.
+        obj.publish(handler)
+
+def _opt_element(handler, name, obj):
+    if obj is None:
+        return
+    _element(handler, name, obj)
+
+
+def _format_date(dt):
+    """convert a datetime into an RFC 822 formatted date
+
+    Input date must be in GMT.
+    """
+    # Looks like:
+    #   Sat, 07 Sep 2002 00:00:01 GMT
+    # Can't use strftime because that's locale dependent
+    #
+    # Isn't there a standard way to do this for Python?  The
+    # rfc822 and email.Utils modules assume a timestamp.  The
+    # following is based on the rfc822 module.
+    return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (
+            ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()],
+            dt.day,
+            ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
+             "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][dt.month-1],
+            dt.year, dt.hour, dt.minute, dt.second)
+
+        
+##
+# A couple simple wrapper objects for the fields which
+# take a simple value other than a string.
+class IntElement:
+    """implements the 'publish' API for integers
+
+    Takes the tag name and the integer value to publish.
+    
+    (Could be used for anything which uses str() to be published
+    to text for XML.)
+    """
+    element_attrs = {}
+    def __init__(self, name, val):
+        self.name = name
+        self.val = val
+    def publish(self, handler):
+        handler.startElement(self.name, self.element_attrs)
+        handler.characters(str(self.val))
+        handler.endElement(self.name)
+
+class DateElement:
+    """implements the 'publish' API for a datetime.datetime
+
+    Takes the tag name and the datetime to publish.
+
+    Converts the datetime to RFC 2822 timestamp (4-digit year).
+    """
+    def __init__(self, name, dt):
+        self.name = name
+        self.dt = dt
+    def publish(self, handler):
+        _element(handler, self.name, _format_date(self.dt))
+####
+
+class Category:
+    """Publish a category element"""
+    def __init__(self, category, domain = None):
+        self.category = category
+        self.domain = domain
+    def publish(self, handler):
+        d = {}
+        if self.domain is not None:
+            d["domain"] = self.domain
+        _element(handler, "category", self.category, d)
+
+class Cloud:
+    """Publish a cloud"""
+    def __init__(self, domain, port, path,
+                 registerProcedure, protocol):
+        self.domain = domain
+        self.port = port
+        self.path = path
+        self.registerProcedure = registerProcedure
+        self.protocol = protocol
+    def publish(self, handler):
+        _element(handler, "cloud", None, {
+            "domain": self.domain,
+            "port": str(self.port),
+            "path": self.path,
+            "registerProcedure": self.registerProcedure,
+            "protocol": self.protocol})
+
+class Image:
+    """Publish a channel Image"""
+    element_attrs = {}
+    def __init__(self, url, title, link,
+                 width = None, height = None, description = None):
+        self.url = url
+        self.title = title
+        self.link = link
+        self.width = width
+        self.height = height
+        self.description = description
+        
+    def publish(self, handler):
+        handler.startElement("image", self.element_attrs)
+
+        _element(handler, "url", self.url)
+        _element(handler, "title", self.title)
+        _element(handler, "link", self.link)
+
+        width = self.width
+        if isinstance(width, int):
+            width = IntElement("width", width)
+        _opt_element(handler, "width", width)
+        
+        height = self.height
+        if isinstance(height, int):
+            height = IntElement("height", height)
+        _opt_element(handler, "height", height)
+
+        _opt_element(handler, "description", self.description)
+
+        handler.endElement("image")
+
+class Guid:
+    """Publish a guid
+
+    Defaults to being a permalink, which is the assumption if it's
+    omitted.  Hence strings are always permalinks.
+    """
+    def __init__(self, guid, isPermaLink = 1):
+        self.guid = guid
+        self.isPermaLink = isPermaLink
+    def publish(self, handler):
+        d = {}
+        if self.isPermaLink:
+            d["isPermaLink"] = "true"
+        else:
+            d["isPermaLink"] = "false"
+        _element(handler, "guid", self.guid, d)
+
+class TextInput:
+    """Publish a textInput
+
+    Apparently this is rarely used.
+    """
+    element_attrs = {}
+    def __init__(self, title, description, name, link):
+        self.title = title
+        self.description = description
+        self.name = name
+        self.link = link
+
+    def publish(self, handler):
+        handler.startElement("textInput", self.element_attrs)
+        _element(handler, "title", self.title)
+        _element(handler, "description", self.description)
+        _element(handler, "name", self.name)
+        _element(handler, "link", self.link)
+        handler.endElement("textInput")
+        
+
+class Enclosure:
+    """Publish an enclosure"""
+    def __init__(self, url, length, type):
+        self.url = url
+        self.length = length
+        self.type = type
+    def publish(self, handler):
+        _element(handler, "enclosure", None,
+                 {"url": self.url,
+                  "length": str(self.length),
+                  "type": self.type,
+                  })
+
+class Source:
+    """Publish the item's original source, used by aggregators"""
+    def __init__(self, name, url):
+        self.name = name
+        self.url = url
+    def publish(self, handler):
+        _element(handler, "source", self.name, {"url": self.url})
+
+class SkipHours:
+    """Publish the skipHours
+
+    This takes a list of hours, as integers.
+    """
+    element_attrs = {}
+    def __init__(self, hours):
+        self.hours = hours
+    def publish(self, handler):
+        if self.hours:
+            handler.startElement("skipHours", self.element_attrs)
+            for hour in self.hours:
+                _element(handler, "hour", str(hour))
+            handler.endElement("skipHours")
+
+class SkipDays:
+    """Publish the skipDays
+
+    This takes a list of days as strings.
+    """
+    element_attrs = {}
+    def __init__(self, days):
+        self.days = days
+    def publish(self, handler):
+        if self.days:
+            handler.startElement("skipDays", self.element_attrs)
+            for day in self.days:
+                _element(handler, "day", day)
+            handler.endElement("skipDays")
+
+class RSS2(WriteXmlMixin):
+    """The main RSS class.
+
+    Stores the channel attributes, with the "category" elements under
+    ".categories" and the RSS items under ".items".
+    """
+    
+    rss_attrs = {"version": "2.0"}
+    element_attrs = {}
+    def __init__(self,
+                 title,
+                 link,
+                 description,
+
+                 language = None,
+                 copyright = None,
+                 managingEditor = None,
+                 webMaster = None,
+                 pubDate = None,  # a datetime, *in* *GMT*
+                 lastBuildDate = None, # a datetime
+                 
+                 categories = None, # list of strings or Category
+                 generator = _generator_name,
+                 docs = "http://blogs.law.harvard.edu/tech/rss",
+                 cloud = None,    # a Cloud
+                 ttl = None,      # integer number of minutes
+
+                 image = None,     # an Image
+                 rating = None,    # a string; I don't know how it's used
+                 textInput = None, # a TextInput
+                 skipHours = None, # a SkipHours with a list of integers
+                 skipDays = None,  # a SkipDays with a list of strings
+
+                 items = None,     # list of RSSItems
+                 ):
+        self.title = title
+        self.link = link
+        self.description = description
+        self.language = language
+        self.copyright = copyright
+        self.managingEditor = managingEditor
+
+        self.webMaster = webMaster
+        self.pubDate = pubDate
+        self.lastBuildDate = lastBuildDate
+        
+        if categories is None:
+            categories = []
+        self.categories = categories
+        self.generator = generator
+        self.docs = docs
+        self.cloud = cloud
+        self.ttl = ttl
+        self.image = image
+        self.rating = rating
+        self.textInput = textInput
+        self.skipHours = skipHours
+        self.skipDays = skipDays
+
+        if items is None:
+            items = []
+        self.items = items
+
+    def publish(self, handler):
+        handler.startElement("rss", self.rss_attrs)
+        handler.startElement("channel", self.element_attrs)
+        _element(handler, "title", self.title)
+        _element(handler, "link", self.link)
+        _element(handler, "description", self.description)
+
+        self.publish_extensions(handler)
+        
+        _opt_element(handler, "language", self.language)
+        _opt_element(handler, "copyright", self.copyright)
+        _opt_element(handler, "managingEditor", self.managingEditor)
+        _opt_element(handler, "webMaster", self.webMaster)
+
+        pubDate = self.pubDate
+        if isinstance(pubDate, datetime.datetime):
+            pubDate = DateElement("pubDate", pubDate)
+        _opt_element(handler, "pubDate", pubDate)
+
+        lastBuildDate = self.lastBuildDate
+        if isinstance(lastBuildDate, datetime.datetime):
+            lastBuildDate = DateElement("lastBuildDate", lastBuildDate)
+        _opt_element(handler, "lastBuildDate", lastBuildDate)
+
+        for category in self.categories:
+            if isinstance(category, basestring):
+                category = Category(category)
+            category.publish(handler)
+
+        _opt_element(handler, "generator", self.generator)
+        _opt_element(handler, "docs", self.docs)
+
+        if self.cloud is not None:
+            self.cloud.publish(handler)
+
+        ttl = self.ttl
+        if isinstance(self.ttl, int):
+            ttl = IntElement("ttl", ttl)
+        _opt_element(handler, "tt", ttl)
+
+        if self.image is not None:
+            self.image.publish(handler)
+
+        _opt_element(handler, "rating", self.rating)
+        if self.textInput is not None:
+            self.textInput.publish(handler)
+        if self.skipHours is not None:
+            self.skipHours.publish(handler)
+        if self.skipDays is not None:
+            self.skipDays.publish(handler)
+
+        for item in self.items:
+            item.publish(handler)
+
+        handler.endElement("channel")
+        handler.endElement("rss")
+
+    def publish_extensions(self, handler):
+        # Derived classes can hook into this to insert
+        # output after the three required fields.
+        pass
+
+    
+    
+class RSSItem(WriteXmlMixin):
+    """Publish an RSS Item"""
+    element_attrs = {}
+    def __init__(self,
+                 title = None,  # string
+                 link = None,   # url as string
+                 description = None, # string
+                 author = None,      # email address as string
+                 categories = None,  # list of string or Category
+                 comments = None,  # url as string
+                 enclosure = None, # an Enclosure
+                 guid = None,    # a unique string
+                 pubDate = None, # a datetime
+                 source = None,  # a Source
+                 ):
+        
+        if title is None and description is None:
+            raise TypeError(
+                "must define at least one of 'title' or 'description'")
+        self.title = title
+        self.link = link
+        self.description = description
+        self.author = author
+        if categories is None:
+            categories = []
+        self.categories = categories
+        self.comments = comments
+        self.enclosure = enclosure
+        self.guid = guid
+        self.pubDate = pubDate
+        self.source = source
+        # It sure does get tedious typing these names three times...
+
+    def publish(self, handler):
+        handler.startElement("item", self.element_attrs)
+        _opt_element(handler, "title", self.title)
+        _opt_element(handler, "link", self.link)
+        self.publish_extensions(handler)
+        _opt_element(handler, "description", self.description)
+        _opt_element(handler, "author", self.author)
+
+        for category in self.categories:
+            if isinstance(category, basestring):
+                category = Category(category)
+            category.publish(handler)
+        
+        _opt_element(handler, "comments", self.comments)
+        if self.enclosure is not None:
+            self.enclosure.publish(handler)
+        _opt_element(handler, "guid", self.guid)
+
+        pubDate = self.pubDate
+        if isinstance(pubDate, datetime.datetime):
+            pubDate = DateElement("pubDate", pubDate)
+        _opt_element(handler, "pubDate", pubDate)
+
+        if self.source is not None:
+            self.source.publish(handler)
+        
+        handler.endElement("item")
+
+    def publish_extensions(self, handler):
+        # Derived classes can hook into this to insert
+        # output after the title and link elements
+        pass
diff --git a/trunk/tools/__init__.py b/trunk/tools/__init__.py
new file mode 100644 (file)
index 0000000..cca19e3
--- /dev/null
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+from station import *
+from xmltodict import *
+from PyRSS2Gen import *
+from mp3 import *
+from ogg import *
+from logger import *
+from player import *
+from recorder import *
+from osc import *
+from twitt import *
+from relay import *
+from tools import *
diff --git a/trunk/tools/get_access_token.py b/trunk/tools/get_access_token.py
new file mode 100644 (file)
index 0000000..de616d1
--- /dev/null
@@ -0,0 +1,91 @@
+#!/usr/bin/python2.4
+# -*- coding: utf-8 -*-
+#
+# Copyright 2007 The Python-Twitter Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import os
+import sys
+
+# parse_qsl moved to urlparse module in v2.6
+try:
+  from urlparse import parse_qsl
+except:
+  from cgi import parse_qsl
+
+import oauth2 as oauth
+
+REQUEST_TOKEN_URL = 'https://twitter.com/oauth/request_token'
+ACCESS_TOKEN_URL  = 'https://twitter.com/oauth/access_token'
+AUTHORIZATION_URL = 'https://twitter.com/oauth/authorize'
+SIGNIN_URL        = 'https://twitter.com/oauth/authenticate'
+
+consumer_key    = 'ozs9cPS2ci6eYQzzMSTb4g'
+consumer_secret = '1kNEffHgGSXO2gMNTr8HRum5s2ofx3VQnJyfd0es'
+
+if consumer_key is None or consumer_secret is None:
+  print 'You need to edit this script and provide values for the'
+  print 'consumer_key and also consumer_secret.'
+  print ''
+  print 'The values you need come from Twitter - you need to register'
+  print 'as a developer your "application".  This is needed only until'
+  print 'Twitter finishes the idea they have of a way to allow open-source'
+  print 'based libraries to have a token that can be used to generate a'
+  print 'one-time use key that will allow the library to make the request'
+  print 'on your behalf.'
+  print ''
+  sys.exit(1)
+
+signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1()
+oauth_consumer             = oauth.Consumer(key=consumer_key, secret=consumer_secret)
+oauth_client               = oauth.Client(oauth_consumer)
+
+print 'Requesting temp token from Twitter'
+
+resp, content = oauth_client.request(REQUEST_TOKEN_URL, 'GET')
+
+if resp['status'] != '200':
+  print 'Invalid respond from Twitter requesting temp token: %s' % resp['status']
+else:
+  request_token = dict(parse_qsl(content))
+
+  print ''
+  print 'Please visit this Twitter page and retrieve the pincode to be used'
+  print 'in the next step to obtaining an Authentication Token:'
+  print ''
+  print '%s?oauth_token=%s' % (AUTHORIZATION_URL, request_token['oauth_token'])
+  print ''
+
+  pincode = raw_input('Pincode? ')
+
+  token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret'])
+  token.set_verifier(pincode)
+
+  print ''
+  print 'Generating and signing request for an access token'
+  print ''
+
+  oauth_client  = oauth.Client(oauth_consumer, token)
+  resp, content = oauth_client.request(ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % pincode)
+  access_token  = dict(parse_qsl(content))
+
+  if resp['status'] != '200':
+    print 'The request for a Token did not succeed: %s' % resp['status']
+    print access_token
+  else:
+    print 'Your Twitter Access Token key: %s' % access_token['oauth_token']
+    print '          Access Token secret: %s' % access_token['oauth_token_secret']
+    print ''
+
diff --git a/trunk/tools/logger.py b/trunk/tools/logger.py
new file mode 100644 (file)
index 0000000..2f68c4d
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import logging
+
+class Logger:
+    """A logging object"""
+
+    def __init__(self, file):
+        self.logger = logging.getLogger('myapp')
+        self.hdlr = logging.FileHandler(file)
+        self.formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
+        self.hdlr.setFormatter(self.formatter)
+        self.logger.addHandler(self.hdlr)
+        self.logger.setLevel(logging.INFO)
+
+    def write_info(self, message):
+        self.logger.info(message)
+
+    def write_error(self, message):
+        self.logger.error(message)
+
diff --git a/trunk/tools/mp3.py b/trunk/tools/mp3.py
new file mode 100644 (file)
index 0000000..4af6c73
--- /dev/null
@@ -0,0 +1,150 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright Guillaume Pellerin (2006-2009)
+
+# <yomguy@parisson.com>
+
+# This software is a computer program whose purpose is to stream audio
+# and video data through icecast2 servers.
+
+# This software is governed by the CeCILL  license under French law and
+# abiding by the rules of distribution of free software.  You can  use,
+# modify and/ or redistribute the software under the terms of the CeCILL
+# license as circulated by CEA, CNRS and INRIA at the following URL
+# "http://www.cecill.info".
+
+# As a counterpart to the access to the source code and  rights to copy,
+# modify and redistribute granted by the license, users are provided only
+# with a limited warranty  and the software's author,  the holder of the
+# economic rights,  and the successive licensors  have only  limited
+# liability.
+
+# In this respect, the user's attention is drawn to the risks associated
+# with loading,  using,  modifying and/or developing or reproducing the
+# software by the user in light of its specific status of free software,
+# that may mean  that it is complicated to manipulate,  and  that  also
+# therefore means  that it is reserved for developers  and  experienced
+# professionals having in-depth computer knowledge. Users are therefore
+# encouraged to load and test the software's suitability as regards their
+# requirements in conditions enabling the security of their systems and/or
+# data to be ensured and,  more generally, to use and operate it in the
+# same conditions as regards security.
+
+# The fact that you are presently reading this means that you have had
+# knowledge of the CeCILL license and that you accept its terms.
+
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+import os
+import string
+import datetime
+from mutagen.easyid3 import EasyID3
+from mutagen.mp3 import MP3, MPEGInfo
+from mutagen import id3
+from tools import *
+
+EasyID3.valid_keys["comment"]="COMM::'XXX'"
+EasyID3.valid_keys["copyright"]="TCOP::'XXX'"
+
+class Mp3:
+    """A MP3 file object"""
+
+    def __init__(self, media):
+        self.media = media
+        self.item_id = ''
+        self.source = self.media
+        self.options = {}
+        self.bitrate_default = '192'
+        self.cache_dir = os.sep + 'tmp'
+        self.keys2id3 = {'title': 'TIT2',
+                    'artist': 'TPE1',
+                    'album': 'TALB',
+                    'date': 'TDRC',
+                    'comment': 'COMM',
+                    'genre': 'TCON',
+                    'copyright': 'TCOP',
+                    }
+        self.mp3 = MP3(self.media, ID3=EasyID3)
+        self.info = self.mp3.info
+        self.bitrate = int(str(self.info.bitrate)[:-3])
+        self.length = datetime.timedelta(0,self.info.length)
+        try:
+            self.metadata = self.get_file_metadata()
+        except:
+            self.metadata = {'title': '',
+                    'artist': '',
+                    'album': '',
+                    'date': '',
+                    'comment': '',
+                    'genre': '',
+                    'copyright': '',
+                    }
+
+        self.description = self.get_description()
+        self.mime_type = self.get_mime_type()
+        self.media_info = get_file_info(self.media)
+        self.file_name = self.media_info[0]
+        self.file_title = self.media_info[1]
+        self.file_ext = self.media_info[2]
+        self.extension = self.get_file_extension()
+        self.size = os.path.getsize(media)
+        #self.args = self.get_args()
+
+    def get_format(self):
+        return 'MP3'
+
+    def get_file_extension(self):
+        return 'mp3'
+
+    def get_mime_type(self):
+        return 'audio/mpeg'
+
+    def get_description(self):
+        return "MPEG audio Layer III"
+
+    def get_file_metadata(self):
+        metadata = {}
+        for key in self.keys2id3.keys():
+            try:
+                metadata[key] = self.mp3[key][0]
+            except:
+                metadata[key] = ''
+        return metadata
+
+    def write_tags(self):
+        """Write all ID3v2.4 tags by mapping dub2id3_dict dictionnary with the
+            respect of mutagen classes and methods"""
+
+        self.mp3.add_tags()
+        self.mp3.tags['TIT2'] = id3.TIT2(encoding=2, text=u'text')
+        self.mp3.save()
+
+        #media_id3 = id3.ID3(self.media)
+        #for tag in self.metadata.keys():
+            #if tag in self.dub2id3_dict.keys():
+                #frame_text = self.dub2id3_dict[tag]
+                #value = self.metadata[tag]
+                #frame = mutagen.id3.Frames[frame_text](3,value)
+            #try:
+                #media_id3.add(frame)
+            #except:
+                #raise IOError('ExporterError: cannot tag "'+tag+'"')
+
+        #try:
+            #media_id3.save()
+        #except:
+            #raise IOError('ExporterError: cannot write tags')
+
+        media = id3.ID3(self.media)
+        media.add(id3.TIT2(encoding=3, text=self.metadata['title'].decode('utf8')))
+        media.add(id3.TP1(encoding=3, text=self.metadata['artist'].decode('utf8')))
+        media.add(id3.TAL(encoding=3, text=self.metadata['album'].decode('utf8')))
+        media.add(id3.TDRC(encoding=3, text=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
+        media.add(id3.TCO(encoding=3, text=self.metadata['genre'].decode('utf8')))
+        try:
+            media.save()
+        except:
+            raise IOError('ExporterError: cannot write tags')
+
+
diff --git a/trunk/tools/ogg.py b/trunk/tools/ogg.py
new file mode 100644 (file)
index 0000000..1f40f1e
--- /dev/null
@@ -0,0 +1,152 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright Guillaume Pellerin (2006-2009)
+
+# <yomguy@parisson.com>
+
+# This software is a computer program whose purpose is to stream audio
+# and video data through icecast2 servers.
+
+# This software is governed by the CeCILL  license under French law and
+# abiding by the rules of distribution of free software.  You can  use,
+# modify and/ or redistribute the software under the terms of the CeCILL
+# license as circulated by CEA, CNRS and INRIA at the following URL
+# "http://www.cecill.info".
+
+# As a counterpart to the access to the source code and  rights to copy,
+# modify and redistribute granted by the license, users are provided only
+# with a limited warranty  and the software's author,  the holder of the
+# economic rights,  and the successive licensors  have only  limited
+# liability.
+
+# In this respect, the user's attention is drawn to the risks associated
+# with loading,  using,  modifying and/or developing or reproducing the
+# software by the user in light of its specific status of free software,
+# that may mean  that it is complicated to manipulate,  and  that  also
+# therefore means  that it is reserved for developers  and  experienced
+# professionals having in-depth computer knowledge. Users are therefore
+# encouraged to load and test the software's suitability as regards their
+# requirements in conditions enabling the security of their systems and/or
+# data to be ensured and,  more generally, to use and operate it in the
+# same conditions as regards security.
+
+# The fact that you are presently reading this means that you have had
+# knowledge of the CeCILL license and that you accept its terms.
+
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+import os
+import string
+import datetime
+from mutagen.oggvorbis import OggVorbis
+from tools import *
+
+
+class Ogg:
+    """An OGG file object"""
+
+    def __init__(self, media):
+        self.media = media
+        self.ogg = OggVorbis(self.media)
+        self.item_id = ''
+        self.source = self.media
+        self.options = {}
+        self.bitrate_default = '192'
+        self.cache_dir = os.sep + 'tmp'
+        self.keys2ogg = {'title': 'title',
+                    'artist': 'artist',
+                    'album': 'album',
+                    'date': 'date',
+                    'comment': 'comment',
+                    'genre': 'genre',
+                    'copyright': 'copyright',
+                    }
+        self.info = self.ogg.info
+        self.bitrate = int(str(self.info.bitrate)[:-3])
+        self.length = datetime.timedelta(0,self.info.length)
+        self.metadata = self.get_file_metadata()
+        self.description = self.get_description()
+        self.mime_type = self.get_mime_type()
+        self.media_info = get_file_info(self.media)
+        self.file_name = self.media_info[0]
+        self.file_title = self.media_info[1]
+        self.file_ext = self.media_info[2]
+        self.extension = self.get_file_extension()
+        self.size = os.path.getsize(media)
+        #self.args = self.get_args()
+
+    def get_format(self):
+        return 'OGG'
+
+    def get_file_extension(self):
+        return 'ogg'
+
+    def get_mime_type(self):
+        return 'application/ogg'
+
+    def get_description(self):
+        return 'FIXME'
+
+    def get_file_info(self):
+        try:
+            file_out1, file_out2 = os.popen4('ogginfo "'+self.dest+'"')
+            info = []
+            for line in file_out2.readlines():
+                info.append(clean_word(line[:-1]))
+            self.info = info
+            return self.info
+        except:
+            raise IOError('ExporterError: file does not exist.')
+
+    def set_cache_dir(self,path):
+       self.cache_dir = path
+
+    def get_file_metadata(self):
+        metadata = {}
+        for key in self.keys2ogg.keys():
+            try:
+                metadata[key] = self.ogg[key][0]
+            except:
+                metadata[key] = ''
+        return metadata
+
+    def decode(self):
+        try:
+            os.system('oggdec -o "'+self.cache_dir+os.sep+self.item_id+
+                      '.wav" "'+self.source+'"')
+            return self.cache_dir+os.sep+self.item_id+'.wav'
+        except:
+            raise IOError('ExporterError: decoder is not compatible.')
+
+    def write_tags(self):
+        self.ogg.add_tags()
+        self.ogg.save()
+        for tag in self.metadata.keys():
+            self.ogg[tag] = str(self.metadata[tag])
+        self.ogg.save()
+
+    def get_args(self,options=None):
+        """Get process options and return arguments for the encoder"""
+        args = []
+        if not options is None:
+            self.options = options
+            if not ('verbose' in self.options and self.options['verbose'] != '0'):
+                args.append('-Q ')
+            if 'ogg_bitrate' in self.options:
+                args.append('-b '+self.options['ogg_bitrate'])
+            elif 'ogg_quality' in self.options:
+                args.append('-q '+self.options['ogg_quality'])
+            else:
+                args.append('-b '+self.bitrate_default)
+        else:
+            args.append('-Q -b '+self.bitrate_default)
+
+        for tag in self.metadata.keys():
+            value = clean_word(self.metadata[tag])
+            args.append('-c %s="%s"' % (tag, value))
+            if tag in self.dub2args_dict.keys():
+                arg = self.dub2args_dict[tag]
+                args.append('-c %s="%s"' % (arg, value))
+
+        return args
diff --git a/trunk/tools/osc.py b/trunk/tools/osc.py
new file mode 100644 (file)
index 0000000..3aa4ff1
--- /dev/null
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Guillaume Pellerin
+
+# <yomguy@parisson.com>
+
+# This software is a computer program whose purpose is to stream audio
+# and video data through icecast2 servers.
+
+# This software is governed by the CeCILL license under French law and
+# abiding by the rules of distribution of free software. You can use,
+# modify and/ or redistribute the software under the terms of the CeCILL
+# license as circulated by CEA, CNRS and INRIA at the following URL
+# "http://www.cecill.info".
+
+# As a counterpart to the access to the source code and  rights to copy,
+# modify and redistribute granted by the license, users are provided only
+# with a limited warranty and the software's author, the holder of the
+# economic rights, and the successive licensors have only limited
+# liability.
+
+# In this respect, the user's attention is drawn to the risks associated
+# with loading, using,  modifying and/or developing or reproducing the
+# software by the user in light of its specific status of free software,
+# that may mean that it is complicated to manipulate, and that also
+# therefore means that it is reserved for developers and  experienced
+# professionals having in-depth computer knowledge. Users are therefore
+# encouraged to load and test the software's suitability as regards their
+# requirements in conditions enabling the security of their systems and/or
+# data to be ensured and, more generally, to use and operate it in the
+# same conditions as regards security.
+
+# The fact that you are presently reading this means that you have had
+# knowledge of the CeCILL license and that you accept its terms.
+
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+from threading import Thread
+
+
+class OSCController(Thread):
+
+    def __init__(self, port):
+        Thread.__init__(self)
+        import liblo
+        self.port = port
+        try:
+            self.server = liblo.Server(self.port)
+        except liblo.ServerError, err:
+            print str(err)
+
+    def add_method(self, path, type, method):
+        self.server.add_method(path, type, method)
+
+    def server(self):
+        return self.server
+
+    def run(self):
+        while True:
+            self.server.recv(100)
diff --git a/trunk/tools/osc_jingles_start.py b/trunk/tools/osc_jingles_start.py
new file mode 100644 (file)
index 0000000..3f2ebf0
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    print str(err)
+    sys.exit()
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/jingles", 1)
diff --git a/trunk/tools/osc_jingles_stop.py b/trunk/tools/osc_jingles_stop.py
new file mode 100644 (file)
index 0000000..d29f721
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    print str(err)
+    sys.exit()
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/jingles", 0)
diff --git a/trunk/tools/osc_player_fast.py b/trunk/tools/osc_player_fast.py
new file mode 100644 (file)
index 0000000..92a60fe
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    print str(err)
+    sys.exit()
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/player", 1)
diff --git a/trunk/tools/osc_player_next.py b/trunk/tools/osc_player_next.py
new file mode 100644 (file)
index 0000000..21a91ee
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    sys.exit(err)
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/media/next", 1)
diff --git a/trunk/tools/osc_player_next2.py b/trunk/tools/osc_player_next2.py
new file mode 100644 (file)
index 0000000..a05c051
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1235)
+except liblo.AddressError, err:
+    sys.exit(err)
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/media/next", 1)
diff --git a/trunk/tools/osc_player_slow.py b/trunk/tools/osc_player_slow.py
new file mode 100644 (file)
index 0000000..02948e0
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    print str(err)
+    sys.exit()
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/player", 0)
diff --git a/trunk/tools/osc_record_start.py b/trunk/tools/osc_record_start.py
new file mode 100644 (file)
index 0000000..779e90b
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    print str(err)
+    sys.exit()
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/record", 1)
diff --git a/trunk/tools/osc_record_stop.py b/trunk/tools/osc_record_stop.py
new file mode 100644 (file)
index 0000000..8056c69
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    print str(err)
+    sys.exit()
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/record", 0)
diff --git a/trunk/tools/osc_relay_start.py b/trunk/tools/osc_relay_start.py
new file mode 100644 (file)
index 0000000..14bcb69
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    print str(err)
+    sys.exit()
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/media/relay", 1)
diff --git a/trunk/tools/osc_relay_stop.py b/trunk/tools/osc_relay_stop.py
new file mode 100644 (file)
index 0000000..eaefe1a
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    print str(err)
+    sys.exit()
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/media/relay", 0)
diff --git a/trunk/tools/osc_run_stop.py b/trunk/tools/osc_run_stop.py
new file mode 100644 (file)
index 0000000..88f43c9
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    print str(err)
+    sys.exit()
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/run", 0)
diff --git a/trunk/tools/osc_twitter_start.py b/trunk/tools/osc_twitter_start.py
new file mode 100644 (file)
index 0000000..c298be6
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    print str(err)
+    sys.exit()
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/twitter", 1)
diff --git a/trunk/tools/osc_twitter_stop.py b/trunk/tools/osc_twitter_stop.py
new file mode 100644 (file)
index 0000000..3470fcc
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import liblo, sys
+
+# send all messages to port 1234 on the local machine
+try:
+    target = liblo.Address(1234)
+except liblo.AddressError, err:
+    print str(err)
+    sys.exit()
+
+# send message "/foo/message1" with int, float and string arguments
+liblo.send(target, "/twitter", 0)
diff --git a/trunk/tools/player.py b/trunk/tools/player.py
new file mode 100644 (file)
index 0000000..9d78d8b
--- /dev/null
@@ -0,0 +1,101 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Guillaume Pellerin
+
+# <yomguy@parisson.com>
+
+# This software is a computer program whose purpose is to stream audio
+# and video data through icecast2 servers.
+
+# This software is governed by the CeCILL license under French law and
+# abiding by the rules of distribution of free software. You can use,
+# modify and/ or redistribute the software under the terms of the CeCILL
+# license as circulated by CEA, CNRS and INRIA at the following URL
+# "http://www.cecill.info".
+
+# As a counterpart to the access to the source code and  rights to copy,
+# modify and redistribute granted by the license, users are provided only
+# with a limited warranty and the software's author, the holder of the
+# economic rights, and the successive licensors have only limited
+# liability.
+
+# In this respect, the user's attention is drawn to the risks associated
+# with loading, using,  modifying and/or developing or reproducing the
+# software by the user in light of its specific status of free software,
+# that may mean that it is complicated to manipulate, and that also
+# therefore means that it is reserved for developers and  experienced
+# professionals having in-depth computer knowledge. Users are therefore
+# encouraged to load and test the software's suitability as regards their
+# requirements in conditions enabling the security of their systems and/or
+# data to be ensured and, more generally, to use and operate it in the
+# same conditions as regards security.
+
+# The fact that you are presently reading this means that you have had
+# knowledge of the CeCILL license and that you accept its terms.
+
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+from relay import *
+
+class Player:
+    """A file streaming iterator"""
+
+    def __init__(self):
+        self.main_buffer_size = 0x100000
+        self.sub_buffer_size = 0x10000
+
+    def set_media(self, media):
+        self.media = media
+
+    def start_relay(self, url):
+        self.url = url
+        self.relay = Relay(self.sub_buffer_size, self.main_buffer_size)
+        self.relay.set_url(self.url)
+        self.relay.open()
+        self.relay.start()
+        self.queue = self.relay.queue
+
+    def stop_relay(self):
+        self.relay.close()
+
+    def file_read_fast(self):
+        """Read media and stream data through a generator."""
+        m = open(self.media, 'r')
+        while True:
+            __main_chunk = m.read(self.sub_buffer_size)
+            if not __main_chunk:
+                break
+            yield __main_chunk
+        m.close()
+
+    def file_read_slow(self):
+        """Read a bigger part of the media and stream the little parts
+         of the data through a generator"""
+        m = open(self.media, 'r')
+        while True:
+            self.main_chunk = m.read(self.main_buffer_size)
+            if not self.main_chunk:
+                break
+            i = 0
+            while True:
+                start = i * self.sub_buffer_size
+                end = self.sub_buffer_size + (i * self.sub_buffer_size)
+                self.sub_chunk = self.main_chunk[start:end]
+                if not self.sub_chunk:
+                    break
+                yield self.sub_chunk
+                i += 1
+        self.main_chunk = 0
+        self.sub_chunk = 0
+        m.close()
+
+    def relay_read(self):
+        """Read a distant media through its URL"""
+        while True:
+            self.sub_chunk = self.queue.get(self.sub_buffer_size)
+            if not self.sub_chunk:
+                break
+            yield self.sub_chunk
+            self.queue.task_done()
+        self.sub_chunk = 0
diff --git a/trunk/tools/recorder.py b/trunk/tools/recorder.py
new file mode 100644 (file)
index 0000000..2f8de3e
--- /dev/null
@@ -0,0 +1,56 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Guillaume Pellerin
+
+# <yomguy@parisson.com>
+
+# This software is a computer program whose purpose is to stream audio
+# and video data through icecast2 servers.
+
+# This software is governed by the CeCILL license under French law and
+# abiding by the rules of distribution of free software. You can use,
+# modify and/ or redistribute the software under the terms of the CeCILL
+# license as circulated by CEA, CNRS and INRIA at the following URL
+# "http://www.cecill.info".
+
+# As a counterpart to the access to the source code and  rights to copy,
+# modify and redistribute granted by the license, users are provided only
+# with a limited warranty and the software's author, the holder of the
+# economic rights, and the successive licensors have only limited
+# liability.
+
+# In this respect, the user's attention is drawn to the risks associated
+# with loading, using,  modifying and/or developing or reproducing the
+# software by the user in light of its specific status of free software,
+# that may mean that it is complicated to manipulate, and that also
+# therefore means that it is reserved for developers and  experienced
+# professionals having in-depth computer knowledge. Users are therefore
+# encouraged to load and test the software's suitability as regards their
+# requirements in conditions enabling the security of their systems and/or
+# data to be ensured and, more generally, to use and operate it in the
+# same conditions as regards security.
+
+# The fact that you are presently reading this means that you have had
+# knowledge of the CeCILL license and that you accept its terms.
+
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+import os
+
+class Recorder:
+    """A file streaming iterator"""
+
+    def __init__(self, path):
+        self.path = path
+
+    def open(self, filename):
+        self.filename = filename
+        self.media = open(self.path + os.sep + self.filename, 'w')
+
+    def write(self, chunk):
+        self.media.write(chunk)
+        self.media.flush()
+
+    def close(self):
+        self.media.close()
diff --git a/trunk/tools/relay.py b/trunk/tools/relay.py
new file mode 100644 (file)
index 0000000..831989e
--- /dev/null
@@ -0,0 +1,75 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Guillaume Pellerin
+
+# <yomguy@parisson.com>
+
+# This software is a computer program whose purpose is to stream audio
+# and video data through icecast2 servers.
+
+# This software is governed by the CeCILL license under French law and
+# abiding by the rules of distribution of free software. You can use,
+# modify and/ or redistribute the software under the terms of the CeCILL
+# license as circulated by CEA, CNRS and INRIA at the following URL
+# "http://www.cecill.info".
+
+# As a counterpart to the access to the source code and  rights to copy,
+# modify and redistribute granted by the license, users are provided only
+# with a limited warranty and the software's author, the holder of the
+# economic rights, and the successive licensors have only limited
+# liability.
+
+# In this respect, the user's attention is drawn to the risks associated
+# with loading, using,  modifying and/or developing or reproducing the
+# software by the user in light of its specific status of free software,
+# that may mean that it is complicated to manipulate, and that also
+# therefore means that it is reserved for developers and  experienced
+# professionals having in-depth computer knowledge. Users are therefore
+# encouraged to load and test the software's suitability as regards their
+# requirements in conditions enabling the security of their systems and/or
+# data to be ensured and, more generally, to use and operate it in the
+# same conditions as regards security.
+
+# The fact that you are presently reading this means that you have had
+# knowledge of the CeCILL license and that you accept its terms.
+
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+from threading import Thread
+import Queue
+import urllib
+
+class Relay(Thread):
+
+    def __init__(self, sub_buffer_size, main_buffer_size):
+        Thread.__init__(self)
+        self.sub_buffer_size = sub_buffer_size
+        self.main_buffer_size = main_buffer_size
+        self.queue = Queue.Queue(self.main_buffer_size)
+
+    def set_url(self, url):
+        self.url = url
+
+    def open(self):
+        try:
+            self.stream = urllib.urlopen(self.url)
+            self.isopen = True
+        except:
+            self.isopen = False
+
+    def close(self):
+        if self.stream:
+            self.isopen = False
+
+    def run(self):
+        while True:
+            if self.isopen:
+                self.chunk = self.stream.read(self.sub_buffer_size)
+                self.queue.put_nowait(self.chunk)
+            else:
+                self.stream.close()
+                break
+
+
+
diff --git a/trunk/tools/station.py b/trunk/tools/station.py
new file mode 100644 (file)
index 0000000..6e19e16
--- /dev/null
@@ -0,0 +1,523 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Guillaume Pellerin
+
+# <yomguy@parisson.com>
+
+# This software is a computer program whose purpose is to stream audio
+# and video data through icecast2 servers.
+
+# This software is governed by the CeCILL license under French law and
+# abiding by the rules of distribution of free software. You can use,
+# modify and/ or redistribute the software under the terms of the CeCILL
+# license as circulated by CEA, CNRS and INRIA at the following URL
+# "http://www.cecill.info".
+
+# As a counterpart to the access to the source code and  rights to copy,
+# modify and redistribute granted by the license, users are provided only
+# with a limited warranty and the software's author, the holder of the
+# economic rights, and the successive licensors have only limited
+# liability.
+
+# In this respect, the user's attention is drawn to the risks associated
+# with loading, using,  modifying and/or developing or reproducing the
+# software by the user in light of its specific status of free software,
+# that may mean that it is complicated to manipulate, and that also
+# therefore means that it is reserved for developers and  experienced
+# professionals having in-depth computer knowledge. Users are therefore
+# encouraged to load and test the software's suitability as regards their
+# requirements in conditions enabling the security of their systems and/or
+# data to be ensured and, more generally, to use and operate it in the
+# same conditions as regards security.
+
+# The fact that you are presently reading this means that you have had
+# knowledge of the CeCILL license and that you accept its terms.
+
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+import os
+import sys
+import time
+import datetime
+import string
+import random
+import shout
+from threading import Thread
+from __init__ import *
+
+
+class Station(Thread):
+    """a DeeFuzzer shouting station thread"""
+
+    def __init__(self, station, q, logger, m3u):
+        Thread.__init__(self)
+        self.station = station
+        self.q = q
+        self.logger = logger
+        self.channel = shout.Shout()
+        self.id = 999999
+        self.counter = 0
+        self.command = 'cat '
+        self.delay = 0
+
+       # Media
+        self.media_dir = self.station['media']['dir']
+        self.channel.format = self.station['media']['format']
+        self.shuffle_mode = int(self.station['media']['shuffle'])
+        self.bitrate = self.station['media']['bitrate']
+        self.ogg_quality = self.station['media']['ogg_quality']
+        self.samplerate = self.station['media']['samplerate']
+        self.voices = self.station['media']['voices']
+
+        # RSS
+        self.rss_dir = self.station['rss']['dir']
+        self.rss_enclosure = self.station['rss']['enclosure']
+
+        # Infos
+        self.channel.url = self.station['infos']['url']
+        self.short_name = self.station['infos']['short_name']
+        self.channel.name = self.station['infos']['name'] + ' : ' + self.channel.url
+        self.channel.genre = self.station['infos']['genre']
+        self.channel.description = self.station['infos']['description']
+        self.base_name = self.rss_dir + os.sep + self.short_name + '_' + self.channel.format
+        self.rss_current_file = self.base_name + '_current.xml'
+        self.rss_playlist_file = self.base_name + '_playlist.xml'
+        self.m3u = m3u
+
+        # Server
+        self.channel.protocol = 'http'     # | 'xaudiocast' | 'icy'
+        self.channel.host = self.station['server']['host']
+        self.channel.port = int(self.station['server']['port'])
+        self.channel.user = 'source'
+        self.channel.password = self.station['server']['sourcepassword']
+        self.channel.mount = '/' + self.short_name + '.' + self.channel.format
+        self.channel.public = int(self.station['server']['public'])
+        self.channel.audio_info = { 'bitrate': self.bitrate,
+                                    'samplerate': self.samplerate,
+                                    'quality': self.ogg_quality,
+                                    'channels': self.voices,}
+        self.playlist = self.get_playlist()
+        self.lp = len(self.playlist)
+        self.channel.open()
+        self.channel_delay = self.channel.delay()
+
+        # Logging
+        self.logger.write_info('Opening ' + self.short_name + ' - ' + self.channel.name + \
+                ' (' + str(self.lp) + ' tracks)...')
+
+        self.metadata_relative_dir = 'metadata'
+        self.metadata_url = self.channel.url + '/rss/' + self.metadata_relative_dir
+        self.metadata_dir = self.rss_dir + os.sep + self.metadata_relative_dir
+        if not os.path.exists(self.metadata_dir):
+            os.makedirs(self.metadata_dir)
+
+        # The station's player
+        self.player = Player()
+        self.player_mode = 0
+
+        # Jingling between each media.
+        # mode = 0 means Off, mode = 1 means On
+        self.jingles_mode = 0
+        if 'jingles' in self.station:
+            self.jingles_mode =  int(self.station['jingles']['mode'])
+            self.jingles_shuffle = self.station['jingles']['shuffle']
+            self.jingles_dir = self.station['jingles']['dir']
+            if self.jingles_mode == 1:
+                self.jingles_callback('/jingles', [1])
+
+        # Relaying
+        # mode = 0 means Off, mode = 1 means On
+        self.relay_mode = 0
+        if 'relay' in self.station:
+            self.relay_mode = int(self.station['relay']['mode'])
+            self.relay_url = self.station['relay']['url']
+            self.relay_author = self.station['relay']['author']
+            if self.relay_mode == 1:
+                self.relay_callback('/relay', [1])
+
+        # Twitting
+        # mode = 0 means Off, mode = 1 means On
+        self.twitter_mode = 0
+        if 'twitter' in self.station:
+            self.twitter_mode = int(self.station['twitter']['mode'])
+            self.twitter_user = self.station['twitter']['user']
+            self.twitter_pass = self.station['twitter']['pass']
+            self.twitter_tags = self.station['twitter']['tags'].split(' ')
+            if self.twitter_mode == 1:
+                self.twitter_callback('/twitter', [1])
+
+        # Recording
+        # mode = 0 means Off, mode = 1 means On
+        self.record_mode = 0
+        if 'record' in self.station:
+            self.record_mode = int(self.station['record']['mode'])
+            self.record_dir = self.station['record']['dir']
+            if not os.path.exists(self.record_dir):
+                os.makedirs(self.record_dir)
+            if self.record_mode == 1:
+                self.record_callback('/record', [1])
+
+        # Running
+        # mode = 0 means Off, mode = 1 means On
+        self.run_mode = 1
+
+        # OSCing
+        self.osc_control_mode = 0
+        # mode = 0 means Off, mode = 1 means On
+        if 'control' in self.station:
+            self.osc_control_mode = int(self.station['control']['mode'])
+            self.osc_port = self.station['control']['port']
+            if self.osc_control_mode == 1:
+                self.osc_controller = OSCController(self.osc_port)
+                self.osc_controller.start()
+                # OSC paths and callbacks
+                self.osc_controller.add_method('/media/next', 'i', self.media_next_callback)
+                self.osc_controller.add_method('/media/relay', 'i', self.relay_callback)
+                self.osc_controller.add_method('/twitter', 'i', self.twitter_callback)
+                self.osc_controller.add_method('/jingles', 'i', self.jingles_callback)
+                self.osc_controller.add_method('/record', 'i', self.record_callback)
+                self.osc_controller.add_method('/player', 'i', self.player_callback)
+                self.osc_controller.add_method('/run', 'i', self.run_callback)
+
+    def run_callback(self, path, value):
+        value = value[0]
+        self.run_mode = value
+        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
+        self.logger.write_info(message)
+
+    def media_next_callback(self, path, value):
+        value = value[0]
+        self.next_media = value
+        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
+        self.logger.write_info(message)
+
+    def relay_callback(self, path, value):
+        value = value[0]
+        if value == 1:
+            self.relay_mode = 1
+            self.player.start_relay(self.relay_url)
+        elif value == 0:
+            self.relay_mode = 0
+            self.player.stop_relay()
+        self.next_media = 1
+        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
+        self.logger.write_info(message)
+        message = "Relaying : %s" % self.relay_url
+        self.logger.write_info(message)
+
+    def twitter_callback(self, path, value):
+        value = value[0]
+        import tinyurl
+        self.twitter = Twitter(self.twitter_user, self.twitter_pass)
+        self.twitter_mode = value
+        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
+        self.m3u_tinyurl = tinyurl.create_one(self.channel.url + '/m3u/' + self.m3u.split(os.sep)[-1])
+        self.rss_tinyurl = tinyurl.create_one(self.channel.url + '/rss/' + self.rss_playlist_file.split(os.sep)[-1])
+        self.logger.write_info(message)
+
+    def jingles_callback(self, path, value):
+        value = value[0]
+        if value == 1:
+            self.jingles_list = self.get_jingles()
+            self.jingles_length = len(self.jingles_list)
+            self.jingle_id = 0
+        self.jingles_mode = value
+        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
+        self.logger.write_info(message)
+
+    def record_callback(self, path, value):
+        value = value[0]
+        if value == 1:
+            self.rec_file = self.short_name + '-' + \
+              datetime.datetime.now().strftime("%x-%X").replace('/', '_') + '.' + self.channel.format
+            self.recorder = Recorder(self.record_dir)
+            self.recorder.open(self.rec_file)
+        elif value == 0:
+            self.recorder.close()
+            if self.channel.format == 'mp3':
+                media = Mp3(self.record_dir + os.sep + self.rec_file)
+            if self.channel.format == 'ogg':
+                media = Ogg(self.record_dir + os.sep + self.rec_file)
+            media.metadata = {'artist': self.artist.encode('utf-8'),
+                                'title': self.title.encode('utf-8'),
+                                'album': self.short_name.encode('utf-8'),
+                                'genre': self.channel.genre.encode('utf-8')}
+            media.write_tags()
+        self.record_mode = value
+        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
+        self.logger.write_info(message)
+
+    def player_callback(self, path, value):
+        value = value[0]
+        self.player_mode = value
+        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
+        self.logger.write_info(message)
+
+    def get_playlist(self):
+        file_list = []
+        for root, dirs, files in os.walk(self.media_dir):
+            for file in files:
+                s = file.split('.')
+                ext = s[len(s)-1]
+                if ext.lower() == self.channel.format and not os.sep+'.' in file:
+                    file_list.append(root + os.sep + file)
+        file_list.sort()
+        return file_list
+
+    def get_jingles(self):
+        file_list = []
+        for root, dirs, files in os.walk(self.jingles_dir):
+            for file in files:
+                s = file.split('.')
+                ext = s[len(s)-1]
+                if ext.lower() == self.channel.format and not os.sep+'.' in file:
+                    file_list.append(root + os.sep + file)
+        file_list.sort()
+        return file_list
+
+    def get_next_media(self):
+        # Init playlist
+        if self.lp != 0:
+            old_playlist = self.playlist
+            new_playlist = self.get_playlist()
+            lp_new = len(new_playlist)
+
+            if lp_new != self.lp or self.counter == 0:
+                # Init playlists
+                self.playlist = new_playlist
+                self.id = 0
+                self.lp = lp_new
+
+                # Twitting new tracks
+                new_playlist_set = set(self.playlist)
+                old_playlist_set = set(old_playlist)
+                new_tracks = new_playlist_set - old_playlist_set
+
+                if len(new_tracks) != 0:
+                    self.new_tracks = list(new_tracks.copy())
+                    new_tracks_objs = self.media_to_objs(self.new_tracks)
+
+                    for media_obj in new_tracks_objs:
+                        title = media_obj.metadata['title']
+                        artist = media_obj.metadata['artist']
+                        if not (title or artist):
+                            song = str(media_obj.file_name)
+                        else:
+                            song = artist + ' : ' + title
+                        song = song.encode('utf-8')
+                        artist = artist.encode('utf-8')
+                        if self.twitter_mode == 1:
+                            artist_names = artist.split(' ')
+                            artist_tags = ' #'.join(list(set(artist_names)-set(['&', '-'])))
+                            message = '#newtrack ! %s #%s on #%s RSS : ' % (song.replace('_', ' '), artist_tags, self.short_name)
+                            message = message[:113] + self.rss_tinyurl
+                            self.update_twitter(message)
+
+                if self.shuffle_mode == 1:
+                    # Shake it, Fuzz it !
+                    random.shuffle(self.playlist)
+
+                self.logger.write_info('Station ' + self.short_name + \
+                                 ' : generating new playlist (' + str(self.lp) + ' tracks)')
+                self.update_rss(self.media_to_objs(self.playlist), self.rss_playlist_file, '(playlist)')
+
+            if self.jingles_mode == 1 and (self.counter % 2) == 0 and not self.jingles_length == 0:
+                media = self.jingles_list[self.jingle_id]
+                self.jingle_id = (self.jingle_id + 1) % self.jingles_length
+            else:
+                media = self.playlist[self.id]
+                self.id = (self.id + 1) % self.lp
+            return media
+        else:
+            mess = 'No media in media_dir !'
+            self.logger.write_error(mess)
+            sys.exit(mess)
+
+    def media_to_objs(self, media_list):
+        media_objs = []
+        for media in media_list:
+            file_name, file_title, file_ext = get_file_info(media)
+            if file_ext.lower() == 'mp3':
+                media_objs.append(Mp3(media))
+            elif file_ext.lower() == 'ogg':
+                media_objs.append(Ogg(media))
+        return media_objs
+
+    def update_rss(self, media_list, rss_file, sub_title):
+        rss_item_list = []
+        if not os.path.exists(self.rss_dir):
+            os.makedirs(self.rss_dir)
+        channel_subtitle = self.channel.name + ' ' + sub_title
+        _date_now = datetime.datetime.now()
+        date_now = str(_date_now)
+        media_absolute_playtime = _date_now
+
+        for media in media_list:
+            media_stats = os.stat(media.media)
+            media_date = time.localtime(media_stats[8])
+            media_date = time.strftime("%a, %d %b %Y %H:%M:%S +0200", media_date)
+            media.metadata['Duration'] = str(media.length).split('.')[0]
+            media.metadata['Bitrate'] = str(media.bitrate) + ' kbps'
+            media.metadata['Next play'] = str(media_absolute_playtime).split('.')[0]
+
+            media_description = '<table>'
+            media_description_item = '<tr><td>%s:   </td><td><b>%s</b></td></tr>'
+            for key in media.metadata.keys():
+                if media.metadata[key] != '':
+                    media_description += media_description_item % (key.capitalize(), media.metadata[key])
+            media_description += '</table>'
+
+            title = media.metadata['title']
+            artist = media.metadata['artist']
+            if not (title or artist):
+                song = str(media.file_title)
+            else:
+                song = artist + ' : ' + title
+
+            media_absolute_playtime += media.length
+
+            if self.rss_enclosure == '1':
+                media_link = self.channel.url + '/media/' + media.file_name
+                media_link = media_link.decode('utf-8')
+                rss_item_list.append(RSSItem(
+                    title = song,
+                    link = media_link,
+                    description = media_description,
+                    enclosure = Enclosure(media_link, str(media.size), 'audio/mpeg'),
+                    guid = Guid(media_link),
+                    pubDate = media_date,)
+                    )
+            else:
+                media_link = self.metadata_url + '/' + media.file_name + '.xml'
+                media_link = media_link.decode('utf-8')
+                rss_item_list.append(RSSItem(
+                    title = song,
+                    link = media_link,
+                    description = media_description,
+                    guid = Guid(media_link),
+                    pubDate = media_date,)
+                    )
+
+        rss = RSS2(title = channel_subtitle,
+                            link = self.channel.url,
+                            description = self.channel.description.decode('utf-8'),
+                            lastBuildDate = date_now,
+                            items = rss_item_list,)
+        f = open(rss_file, 'w')
+        rss.write_xml(f, 'utf-8')
+        f.close()
+
+    def update_twitter(self, message):
+        try:
+            self.twitter.post(message.decode('utf8'))
+            self.logger.write_info('Twitting : "' + message + '"')
+        except:
+            self.logger.write_error('Twitting : "' + message + '"')
+            pass
+
+    def set_relay_mode(self):
+        self.prefix = '#nowplaying (relaying #LIVE)'
+        self.title = self.channel.description.encode('utf-8')
+        self.artist = self.relay_author.encode('utf-8')
+        self.title = self.title.replace('_', ' ')
+        self.artist = self.artist.replace('_', ' ')
+        self.song = self.artist + ' : ' + self.title
+        #self.channel.set_metadata({'song': self.song, 'charset': 'utf8',})
+        self.stream = self.player.relay_read()
+
+    def set_read_mode(self):
+        self.prefix = '#nowplaying'
+        self.current_media_obj = self.media_to_objs([self.media])
+        self.title = self.current_media_obj[0].metadata['title']
+        self.artist = self.current_media_obj[0].metadata['artist']
+        self.title = self.title.replace('_', ' ')
+        self.artist = self.artist.replace('_', ' ')
+        if not (self.title or self.artist):
+            song = str(self.current_media_obj[0].file_name)
+        else:
+            song = self.artist + ' : ' + self.title
+        self.song = song.encode('utf-8')
+        self.artist = self.artist.encode('utf-8')
+        self.metadata_file = self.metadata_dir + os.sep + self.current_media_obj[0].file_name + '.xml'
+        self.update_rss(self.current_media_obj, self.metadata_file, '')
+        #self.channel.set_metadata({'song': self.song, 'charset': 'utf8',})
+        self.update_rss(self.current_media_obj, self.rss_current_file, '(currently playing)')
+        self.logger.write_info('Deefuzzing on %s :  id = %s, name = %s' \
+            % (self.short_name, self.id, self.current_media_obj[0].file_name))
+        self.player.set_media(self.media)
+        if self.player_mode == 0:
+            self.stream = self.player.file_read_slow()
+        elif self.player_mode == 1:
+            self.stream = self.player.file_read_fast()
+
+    def update_twitter_current(self):
+        artist_names = self.artist.split(' ')
+        artist_tags = ' #'.join(list(set(artist_names)-set(['&', '-'])))
+        message = '♫ %s %s on #%s #%s' % (self.prefix, self.song, self.short_name, artist_tags)
+        tags = '#' + ' #'.join(self.twitter_tags)
+        message = message + ' ' + tags
+        message = message[:107] + ' M3U : ' + self.m3u_tinyurl
+        self.update_twitter(message)
+
+    def run(self):
+        while self.run_mode:
+            self.q.get(1)
+            self.next_media = 0
+            self.media = self.get_next_media()
+            self.counter += 1
+            if self.relay_mode:
+                self.set_relay_mode()
+            elif os.path.exists(self.media) and not os.sep+'.' in self.media:
+                if self.lp == 0:
+                    self.logger.write_error('Station ' + self.short_name + ' has no media to stream !')
+                    break
+                self.set_read_mode()
+            self.q.task_done()
+
+            self.q.get(1)
+            if (not (self.jingles_mode and (self.counter % 2)) or self.relay_mode) and self.twitter_mode:
+                self.update_twitter_current()
+            self.channel.set_metadata({'song': self.song, 'charset': 'utf-8',})
+            self.q.task_done()
+
+            for self.chunk in self.stream:
+                self.q.get(1)
+
+                if self.next_media or not self.run_mode:
+                    break
+
+                try:
+                    if self.record_mode:
+                        self.recorder.write(self.chunk)
+                except:
+                    self.logger.write_error('Station ' + self.short_name + ' : could not write the buffer to the file')
+                    continue
+
+                try:
+                    self.channel.send(self.chunk)
+                    self.channel.sync()
+                except:
+                    self.logger.write_error('Station ' + self.short_name + ' : could not send the buffer')
+                    try:
+                        self.channel.close()
+                    except:
+                        self.logger.write_error('Station ' + self.short_name + ' : could not close the channel')
+                        try:
+                            self.channel.open()
+                            self.channel.send(self.chunk)
+                            self.channel.sync()
+                            self.channel.set_metadata({'song': self.song, 'charset': 'utf8',})
+                            self.channel.sync()
+                        except:
+                            self.logger.write_error('Station ' + self.short_name + ' : could not restart the channel')
+                            continue
+                        continue
+                    continue
+
+                self.q.task_done()
+
+        if self.record_mode:
+            self.recorder.close()
+
+        self.channel.close()
diff --git a/trunk/tools/tools.py b/trunk/tools/tools.py
new file mode 100644 (file)
index 0000000..8282cfc
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2007-2009 Guillaume Pellerin <yomguy@parisson.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://svn.parisson.org/deefuzz/wiki/DefuzzLicense.
+#
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+import os
+import re
+import string
+
+def clean_word(word) :
+    """ Return the word without excessive blank spaces, underscores and
+    characters causing problem to exporters"""
+    word = re.sub("^[^\w]+","",word)    #trim the beginning
+    word = re.sub("[^\w]+$","",word)    #trim the end
+    word = re.sub("_+","_",word)        #squeeze continuous _ to one _
+    word = re.sub("^[^\w]+","",word)    #trim the beginning _
+    #word = string.replace(word,' ','_')
+    #word = string.capitalize(word)
+    dict = '&[];"*:,'
+    for letter in dict:
+        word = string.replace(word,letter,'_')
+    return word
+
+def get_file_info(media):
+    file_name = media.split(os.sep)[-1]
+    file_title = file_name.split('.')[:-1]
+    file_title = '.'.join(file_title)
+    file_ext = file_name.split('.')[-1]
+    return file_name, file_title, file_ext
diff --git a/trunk/tools/twitt.py b/trunk/tools/twitt.py
new file mode 100644 (file)
index 0000000..ad88a21
--- /dev/null
@@ -0,0 +1,62 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Guillaume Pellerin
+
+# <yomguy@parisson.com>
+
+# This software is a computer program whose purpose is to stream audio
+# and video data through icecast2 servers.
+
+# This software is governed by the CeCILL license under French law and
+# abiding by the rules of distribution of free software. You can use,
+# modify and/ or redistribute the software under the terms of the CeCILL
+# license as circulated by CEA, CNRS and INRIA at the following URL
+# "http://www.cecill.info".
+
+# As a counterpart to the access to the source code and  rights to copy,
+# modify and redistribute granted by the license, users are provided only
+# with a limited warranty and the software's author, the holder of the
+# economic rights, and the successive licensors have only limited
+# liability.
+
+# In this respect, the user's attention is drawn to the risks associated
+# with loading, using,  modifying and/or developing or reproducing the
+# software by the user in light of its specific status of free software,
+# that may mean that it is complicated to manipulate, and that also
+# therefore means that it is reserved for developers and  experienced
+# professionals having in-depth computer knowledge. Users are therefore
+# encouraged to load and test the software's suitability as regards their
+# requirements in conditions enabling the security of their systems and/or
+# data to be ensured and, more generally, to use and operate it in the
+# same conditions as regards security.
+
+# The fact that you are presently reading this means that you have had
+# knowledge of the CeCILL license and that you accept its terms.
+
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+# Twitter DeeFuzzer keys
+TWITTER_CONSUMER_KEY = 'ozs9cPS2ci6eYQzzMSTb4g'
+TWITTER_CONSUMER_SECRET = '1kNEffHgGSXO2gMNTr8HRum5s2ofx3VQnJyfd0es'
+
+class Twitter:
+
+    def __init__(self, access_token_key, access_token_secret):
+        import twitter
+        self.username = TWITTER_CONSUMER_KEY
+        self.password = TWITTER_CONSUMER_SECRET
+        self.access_token_key = access_token_key
+        self.access_token_secret = access_token_secret
+        self.api = twitter.Api(username=self.username,
+                               password=self.password,
+                               access_token_key=access_token_key,
+                               access_token_secret=access_token_secret)
+
+    def post(self, message):
+        try:
+            self.api.PostUpdate(message)
+        except:
+            pass
+
+
diff --git a/trunk/tools/xmltodict.py b/trunk/tools/xmltodict.py
new file mode 100644 (file)
index 0000000..b85d556
--- /dev/null
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+# Easily import simple XML data to Python dictionary
+# http://www.gmta.info/publications/parsing-simple-xml-structure-to-a-python-dictionary
+
+import xml.dom.minidom
+
+def haschilds(dom):
+    # Checks whether an element has any childs
+    # containing real tags opposed to just text.
+    for childnode in dom.childNodes:
+        if childnode.nodeName != "#text" and \
+            childnode.nodeName != "#cdata-section":
+            return True
+    return False
+
+def indexchilds(dom, enc):
+    childsdict = dict()
+    for childnode in dom.childNodes:
+        name = childnode.nodeName.encode(enc)
+        if name == "#text" or name == "#cdata-section":
+            # ignore whitespaces
+            continue
+        if haschilds(childnode):
+            v = indexchilds(childnode, enc)
+        else:
+            v = childnode.childNodes[0].nodeValue.encode(enc)
+        if name in childsdict:
+            if isinstance(childsdict[name], dict):
+                # there is multiple instances of this node - convert to list
+                childsdict[name] = [childsdict[name]]
+            childsdict[name].append(v)
+        else:
+            childsdict[name] = v
+    return childsdict
+
+def xmltodict(data, enc=None):
+    dom = xml.dom.minidom.parseString(data.strip())
+    return indexchilds(dom, enc)
+
+
diff --git a/trunk/tools/xmltodict2.py b/trunk/tools/xmltodict2.py
new file mode 100644 (file)
index 0000000..b669239
--- /dev/null
@@ -0,0 +1,397 @@
+# -*- coding: utf-8 -*-
+""" xmltodict(): convert xml into tree of Python dicts.
+
+This was copied and modified from John Bair's recipe at aspn.activestate.com:
+       http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/149368
+"""
+import os
+import string
+import locale
+from xml.parsers import expat
+
+# If we're in Dabo, get the default encoding.
+#import dabo
+#import dabo.lib.DesignerUtils as desUtil
+#from dabo.dLocalize import _
+#from dabo.lib.utils import resolvePath
+#app = dabo.dAppRef
+#if app is not None:
+       #default_encoding = app.Encoding
+#else:
+               #enc = locale.getlocale()[1]
+               #if enc is None:
+                       #enc = dabo.defaultEncoding
+               #default_encoding = enc
+
+# Python seems to need to compile code with \n linesep:
+code_linesep = "\n"
+eol = os.linesep
+
+
+class Xml2Obj:
+       """XML to Object"""
+       def __init__(self):
+               self.root = None
+               self.nodeStack = []
+               self.attsToSkip = []
+               self._inCode = False
+               self._mthdName = ""
+               self._mthdCode = ""
+               self._codeDict = None
+               self._inProp = False
+               self._propName = ""
+               self._propData = ""
+               self._propDict = None
+               self._currPropAtt = ""
+               self._currPropDict = None
+
+
+       def StartElement(self, name, attributes):
+               """SAX start element even handler"""
+               if name == "code":
+                       # This is code for the parent element
+                       self._inCode = True
+                       parent = self.nodeStack[-1]
+                       if not parent.has_key("code"):
+                               parent["code"] = {}
+                               self._codeDict = parent["code"]
+
+               elif name == "properties":
+                       # These are the custom property definitions
+                       self._inProp = True
+                       self._propName = ""
+                       self._propData = ""
+                       parent = self.nodeStack[-1]
+                       if not parent.has_key("properties"):
+                               parent["properties"] = {}
+                               self._propDict = parent["properties"]
+
+               else:
+                       if self._inCode:
+                               self._mthdName = name.encode()
+                       elif self._inProp:
+                               if self._propName:
+                                       # In the middle of a prop definition
+                                       self._currPropAtt = name.encode()
+                               else:
+                                       self._propName = name.encode()
+                                       self._currPropDict = {}
+                                       self._currPropAtt = ""
+                       else:
+                               element = {"name": name.encode()}
+                               if len(attributes) > 0:
+                                       for att in self.attsToSkip:
+                                               if attributes.has_key(att):
+                                                       del attributes[att]
+                                       element["attributes"] = attributes
+
+                               # Push element onto the stack and make it a child of parent
+                               if len(self.nodeStack) > 0:
+                                       parent = self.nodeStack[-1]
+                                       if not parent.has_key("children"):
+                                               parent["children"] = []
+                                       parent["children"].append(element)
+                               else:
+                                       self.root = element
+                               self.nodeStack.append(element)
+
+
+       def EndElement(self, name):
+               """SAX end element event handler"""
+               if self._inCode:
+                       if name == "code":
+                               self._inCode = False
+                               self._codeDict = None
+                       else:
+                               # End of an individual method
+                               mth = self._mthdCode.strip()
+                               if not mth.endswith("\n"):
+                                       mth += "\n"
+                               self._codeDict[self._mthdName] = mth
+                               self._mthdName = ""
+                               self._mthdCode = ""
+               elif self._inProp:
+                       if name == "properties":
+                               self._inProp = False
+                               self._propDict = None
+                       elif name == self._propName:
+                               # End of an individual prop definition
+                               self._propDict[self._propName] = self._currPropDict
+                               self._propName = ""
+                       else:
+                               # end of a property attribute
+                               self._currPropDict[self._currPropAtt] = self._propData
+                               self._propData = self._currPropAtt = ""
+               else:
+                       self.nodeStack = self.nodeStack[:-1]
+
+
+       def CharacterData(self, data):
+               """SAX character data event handler"""
+               if self._inCode or data.strip():
+                       data = data.replace("&lt;", "<")
+                       data = data.encode()
+                       if self._inCode:
+                               if self._mthdCode:
+                                       self._mthdCode += data
+                               else:
+                                       self._mthdCode = data
+                       elif self._inProp:
+                               self._propData += data
+                       else:
+                               element = self.nodeStack[-1]
+                               if not element.has_key("cdata"):
+                                       element["cdata"] = ""
+                               element["cdata"] += data
+
+
+       def Parse(self, xml):
+               # Create a SAX parser
+               Parser = expat.ParserCreate()
+               # SAX event handlers
+               Parser.StartElementHandler = self.StartElement
+               Parser.EndElementHandler = self.EndElement
+               Parser.CharacterDataHandler = self.CharacterData
+               # Parse the XML File
+               ParserStatus = Parser.Parse(xml, 1)
+               return self.root
+
+
+       def ParseFromFile(self, filename):
+               return self.Parse(open(filename,"r").read())
+
+
+def xmltodict(xml, attsToSkip=[], addCodeFile=False):
+       """Given an xml string or file, return a Python dictionary."""
+       parser = Xml2Obj()
+       parser.attsToSkip = attsToSkip
+       isPath = os.path.exists(xml)
+       errmsg = ""
+       if eol not in xml and isPath:
+               # argument was a file
+               try:
+                       ret = parser.ParseFromFile(xml)
+               except expat.ExpatError, e:
+                       errmsg = _("The XML in '%s' is not well-formed and cannot be parsed: %s") % (xml, e)
+       else:
+               # argument must have been raw xml:
+               if not xml.strip().startswith("<?xml "):
+                       # it's a bad file name
+                       errmsg = _("The file '%s' could not be found") % xml
+               else:
+                       try:
+                               ret = parser.Parse(xml)
+                       except expat.ExpatError:
+                               errmsg = _("An invalid XML string was encountered")
+       if errmsg:
+               raise dabo.dException.XmlException, errmsg
+       if addCodeFile and isPath:
+               # Get the associated code file, if any
+               codePth = "%s-code.py" % os.path.splitext(xml)[0]
+               if os.path.exists(codePth):
+                       try:
+                               codeDict = desUtil.parseCodeFile(open(codePth).read())
+                               desUtil.addCodeToClassDict(ret, codeDict)
+                       except StandardError, e:
+                               print "Failed to parse code file:", e
+       return ret
+
+
+def escQuote(val, noEscape=False, noQuote=False):
+       """Add surrounding quotes to the string, and escape
+       any illegal XML characters.
+       """
+       if not isinstance(val, basestring):
+               val = str(val)
+       if not isinstance(val, unicode):
+               val = unicode(val, default_encoding)
+       if noQuote:
+               qt = ''
+       else:
+               qt = '"'
+       slsh = "\\"
+#      val = val.replace(slsh, slsh+slsh)
+       if not noEscape:
+               # First escape internal ampersands. We need to double them up due to a
+               # quirk in wxPython and the way it displays this character.
+               val = val.replace("&", "&amp;&amp;")
+               # Escape any internal quotes
+               val = val.replace('"', '&quot;').replace("'", "&apos;")
+               # Escape any high-order characters
+               chars = []
+               for pos, char in enumerate(list(val)):
+                       if ord(char) > 127:
+                               chars.append("&#%s;" % ord(char))
+                       else:
+                                       chars.append(char)
+               val = "".join(chars)
+       val = val.replace("<", "&#060;").replace(">", "&#062;")
+       return "%s%s%s" % (qt, val, qt)
+
+
+def dicttoxml(dct, level=0, header=None, linesep=None):
+       """Given a Python dictionary, return an xml string.
+
+       The dictionary must be in the format returned by dicttoxml(), with keys
+       on "attributes", "code", "cdata", "name", and "children".
+
+       Send your own XML header, otherwise a default one will be used.
+
+       The linesep argument is a dictionary, with keys on levels, allowing the
+       developer to add extra whitespace depending on the level.
+       """
+       att = ""
+       ret = ""
+
+       if dct.has_key("attributes"):
+               for key, val in dct["attributes"].items():
+                       # Some keys are already handled.
+                       noEscape = key in ("sizerInfo",)
+                       val = escQuote(val, noEscape)
+                       att += " %s=%s" % (key, val)
+       ret += "%s<%s%s" % ("\t" * level, dct["name"], att)
+
+       if (not dct.has_key("cdata") and not dct.has_key("children")
+                       and not dct.has_key("code") and not dct.has_key("properties")):
+               ret += " />%s" % eol
+       else:
+               ret += ">"
+               if dct.has_key("cdata"):
+                       ret += "%s" % dct["cdata"].replace("<", "&lt;")
+
+               if dct.has_key("code"):
+                       if len(dct["code"].keys()):
+                               ret += "%s%s<code>%s" % (eol, "\t" * (level+1), eol)
+                               methodTab = "\t" * (level+2)
+                               for mthd, cd in dct["code"].items():
+                                       # Convert \n's in the code to eol:
+                                       cd = eol.join(cd.splitlines())
+
+                                       # Make sure that the code ends with a linefeed
+                                       if not cd.endswith(eol):
+                                               cd += eol
+
+                                       ret += "%s<%s><![CDATA[%s%s]]>%s%s</%s>%s" % (methodTab,
+                                                       mthd, eol, cd, eol,
+                                                       methodTab, mthd, eol)
+                               ret += "%s</code>%s"    % ("\t" * (level+1), eol)
+
+               if dct.has_key("properties"):
+                       if len(dct["properties"].keys()):
+                               ret += "%s%s<properties>%s" % (eol, "\t" * (level+1), eol)
+                               currTab = "\t" * (level+2)
+                               for prop, val in dct["properties"].items():
+                                       ret += "%s<%s>%s" % (currTab, prop, eol)
+                                       for propItm, itmVal in val.items():
+                                               itmTab = "\t" * (level+3)
+                                               ret += "%s<%s>%s</%s>%s" % (itmTab, propItm, itmVal,
+                                                               propItm, eol)
+                                       ret += "%s</%s>%s" % (currTab, prop, eol)
+                               ret += "%s</properties>%s"      % ("\t" * (level+1), eol)
+
+               if dct.has_key("children") and len(dct["children"]) > 0:
+                       ret += eol
+                       for child in dct["children"]:
+                               ret += dicttoxml(child, level+1, linesep=linesep)
+               indnt = ""
+               if ret.endswith(eol):
+                       # Indent the closing tag
+                       indnt = ("\t" * level)
+               ret += "%s</%s>%s" % (indnt, dct["name"], eol)
+
+               if linesep:
+                       ret += linesep.get(level, "")
+
+       if level == 0:
+               if header is None:
+                       header = '<?xml version="1.0" encoding="%s" standalone="no"?>%s' \
+                                       % (default_encoding, eol)
+               ret = header + ret
+
+       return ret
+
+
+def flattenClassDict(cd, retDict=None):
+       """Given a dict containing a series of nested objects such as would
+       be created by restoring from a cdxml file, returns a dict with all classIDs
+       as keys, and a dict as the corresponding value. The dict value will have
+       keys for the attributes and/or code, depending on what was in the original
+       dict. The end result is to take a nested dict structure and return a flattened
+       dict with all objects at the top level.
+       """
+       if retDict is None:
+               retDict = {}
+       atts = cd.get("attributes", {})
+       props = cd.get("properties", {})
+       kids = cd.get("children", [])
+       code = cd.get("code", {})
+       classID = atts.get("classID", "")
+       classFile = resolvePath(atts.get("designerClass", ""))
+       superclass = resolvePath(atts.get("superclass", ""))
+       superclassID = atts.get("superclassID", "")
+       if superclassID and os.path.exists(superclass):
+               # Get the superclass info
+               superCD = xmltodict(superclass, addCodeFile=True)
+               flattenClassDict(superCD, retDict)
+       if classID:
+               if os.path.exists(classFile):
+                       # Get the class info
+                       classCD = xmltodict(classFile, addCodeFile=True)
+                       classAtts = classCD.get("attributes", {})
+                       classProps = classCD.get("properties", {})
+                       classCode = classCD.get("code", {})
+                       classKids = classCD.get("children", [])
+                       currDict = retDict.get(classID, {})
+                       retDict[classID] = {"attributes": classAtts, "code": classCode,
+                                       "properties": classProps}
+                       retDict[classID].update(currDict)
+                       # Now update the child objects in the dict
+                       for kid in classKids:
+                               flattenClassDict(kid, retDict)
+               else:
+                       # Not a file; most likely just a component in another class
+                       currDict = retDict.get(classID, {})
+                       retDict[classID] = {"attributes": atts, "code": code,
+                                       "properties": props}
+                       retDict[classID].update(currDict)
+       if kids:
+               for kid in kids:
+                       flattenClassDict(kid, retDict)
+       return retDict
+
+
+def addInheritedInfo(src, super, updateCode=False):
+       """Called recursively on the class container structure, modifying
+       the attributes to incorporate superclass information. When the
+       'updateCode' parameter is True, superclass code is added to the
+       object's code
+       """
+       atts = src.get("attributes", {})
+       props = src.get("properties", {})
+       kids = src.get("children", [])
+       code = src.get("code", {})
+       classID = atts.get("classID", "")
+       if classID:
+               superInfo = super.get(classID, {"attributes": {}, "code": {}, "properties": {}})
+               src["attributes"] = superInfo["attributes"].copy()
+               src["attributes"].update(atts)
+               src["properties"] = superInfo.get("properties", {}).copy()
+               src["properties"].update(props)
+               if updateCode:
+                       src["code"] = superInfo["code"].copy()
+                       src["code"].update(code)
+       if kids:
+               for kid in kids:
+                       addInheritedInfo(kid, super, updateCode)
+
+
+
+#if __name__ == "__main__":
+       #test_dict = {"name": "test", "attributes":{"path": "c:\\temp\\name",
+                       #"problemChars": "Welcome to <Jos\xc3\xa9's \ Stuff!>\xc2\xae".decode("latin-1")}}
+       #print "test_dict:", test_dict
+       #xml = dicttoxml(test_dict)
+       #print "xml:", xml
+       #test_dict2 = xmltodict(xml)
+       #print "test_dict2:", test_dict2
+       #print "same?:", test_dict == test_dict2