]> git.parisson.com Git - deefuzzer.git/commitdiff
rename it DeeFuzzer
authorGuillaume Pellerin <yomguy@parisson.com>
Mon, 4 May 2009 07:47:56 +0000 (07:47 +0000)
committerGuillaume Pellerin <yomguy@parisson.com>
Mon, 4 May 2009 07:47:56 +0000 (07:47 +0000)
COPYING
INSTALL
README
deefuzz-deamon.sh [deleted file]
deefuzz.py [deleted file]
deefuzzer-deamon.sh [new file with mode: 0755]
deefuzzer.py [new file with mode: 0755]
example/myfuzz.xml
example/test_mp3.xml
example/test_mp3_8.xml

diff --git a/COPYING b/COPYING
index 33cd1ec395d8db92bd66dc90703483c9939c2153..ad5f994165bbe8b49aca524fba1ef8bad6cc1195 100644 (file)
--- a/COPYING
+++ b/COPYING
@@ -18,7 +18,7 @@ the two main principles guiding its drafting:
 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
+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.
 
@@ -326,7 +326,7 @@ The Licensee expressly undertakes:
 
 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
+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.
 
diff --git a/INSTALL b/INSTALL
index 0fec0922a9e8dd78f47232cd23f2bec27f2f45f7..a02ecbda394a793684d51357aa01ce9b1d31c1ac 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -5,7 +5,7 @@
 #
 # 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/DeeFuzzLicense.
+# are also available at http://svn.parisson.org/deefuzzer/DeeFuzzLicense.
 #
 # Author: Guillaume Pellerin <pellerin@parisson.com>
 
  provides : shout-python
 
 python-shout is included in DeeFuzz but needs to be compiled and installed
-if you don't install deefuzz from its debian package with aptitude.
+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 deefuzz (from the main deefuzz directory) :
+To install deefuzzer (from the main deefuzzer directory) :
 
   $ sudo python install.py
 
-For more informations, see http://svn.parisson.org/deefuzz/
\ No newline at end of file
+For more informations, see http://svn.parisson.org/deefuzzer/
\ No newline at end of file
diff --git a/README b/README
index cea32df5c1c47383df20752c59562399e1be4157..e580d44281ac207f29d7fbd8a65a8446f9a3cc60 100644 (file)
--- a/README
+++ b/README
@@ -1,15 +1,15 @@
 # README
 # ======
 
-deefuzz : an easy and light media streaming tools
+deefuzzer : an easy and light media streaming tools
 
 
 # 1. Introduction
 # ===============
 
-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. It depends on python, icecast2 server, libshout and some other tools.
+DeeFuzzer 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. It depends on python, icecast2 server, libshout and some other tools.
 
-Here are the main features of the DeeFuzz Tools:
+Here are the main features of the DeeFuzzer Tools:
 
  * MP3 and OGG (audio & video) file streaming over internet
  * Full metadata encapsulation and management
@@ -34,34 +34,34 @@ see INSTALL
 
  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/DeeFuzzLicense
+ are also available at http://svn.parisson.org/deefuzzer/DeeFuzzerLicense
 
 
 # 4. Usage
 # =========
 
-Usage : deefuzz CONFIGFILE
+Usage : deefuzzer CONFIGFILE
   
   where CONFIGFILE is the path for a XML config file. For example:
 
-    $ deefuzz example/myfuzz.xml
+    $ deefuzzer example/myfuzz.xml
 
 Note that you must edit the config file with right parameters before executing.
 You can find an example for the XML file in the directory "example/" of this
-application (maybe in /usr/share/deefuzz if installed with the help of install.py)
+application (maybe in /usr/share/deefuzzer if installed with the help of install.py)
 
-DeeFuzz can also be run as a deamon using deefuzz-deamon:
+DeeFuzzer can also be run as a deamon using deefuzzer-deamon:
 
-    $ deefuzz-deamon example/myfuzz.xml &
+    $ deefuzzer-deamon example/myfuzz.xml &
 
-Since 0.3, deefuzz doesn't print anything into the shell, then a right <log> parameter
+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-threading implementation of deefuzz instances
+BE CAREFULL : at the moment, the multi-threading implementation of deefuzzer instances
 avoids to shut down the streams with CTRL+C... You have to kill them manually,
 after a CTRL+Z, making this:
 
-    $ kill -9 `pgrep deefuzz`
+    $ kill -9 `pgrep deefuzzer`
 
 
 # 5. Author
@@ -89,5 +89,5 @@ See COPYING
 # 8. Contact / Infos
 # ==================
 
-see http://svn.parisson.org/deefuzz/ or http://parisson.com for more info.
+see http://svn.parisson.org/deefuzzer/ or http://parisson.com for more info.
 
diff --git a/deefuzz-deamon.sh b/deefuzz-deamon.sh
deleted file mode 100755 (executable)
index 8343ea9..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-
-conf_file=$1
-log_file=/tmp/deefuzz.log
-
-set -e
-ulimit -c unlimited
-while true; do
-  deefuzz $1 > /dev/null
-  sleep 3
-done
diff --git a/deefuzz.py b/deefuzz.py
deleted file mode 100755 (executable)
index 6ebc49a..0000000
+++ /dev/null
@@ -1,457 +0,0 @@
-#!/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 sys
-import time
-import datetime
-import string
-import random
-import Queue
-import shout
-import subprocess
-from threading import Thread
-from tools import *
-
-version = '0.3.0'
-year = datetime.datetime.now().strftime("%Y")
-
-
-def prog_info():
-        desc = '\n deefuzz : easy and light streaming tool\n'
-        ver = ' version : %s \n\n' % (version)
-        info = """ 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/d-fuzz/DeeFuzzLicense
-
- depends : python, python-xml, python-shout, libshout3, icecast2
- recommends : python-mutagen
- provides : python-shout
-
- Usage : deefuzz $1
-  where $1 is the path for a XML config file
-  ex: deefuzz example/myfuzz.xml
- see http://svn.parisson.org/deefuzz/ for more details
-        """ % (year)
-        text = desc + ver + info
-        return text
-
-
-class DeeFuzzError:
-    """The DeeFuzz main error class"""
-    def __init__(self, message):
-        self.message = message
-    def __str__(self):
-        return 'DeeFuzz Error : ' + self.message
-
-
-class DeeFuzzStreamError:
-    """The DeeFuzz stream error class"""
-    def __init__(self, message, command, subprocess):
-        self.message = message
-        self.command = str(command)
-        self.subprocess = subprocess
-
-    def __str__(self):
-        if self.subprocess.stderr != None:
-            error = self.subprocess.stderr.read()
-        else:
-            error = ''
-        return "%s ; command: %s; error: %s" % (self.message,
-                                                self.command,
-                                                error)
-
-
-class DeeFuzz(Thread):
-    """a DeeFuzz 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['deefuzz'].keys():
-            self.logger = Logger(self.conf['deefuzz']['log'])
-        else:
-            self.logger = Logger('.' + os.sep + 'deefuzz.log')
-        if 'm3u' in self.conf['deefuzz'].keys():
-            self.m3u = self.conf['deefuzz']['m3u']
-        else:
-            self.m3u = '.' + os.sep + 'deefuzz.m3u'
-
-    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')
-        i = 1
-        m3u.write('#EXTM3U\n')
-        for s in self.stations:
-            info = '#EXTINF:%s,%s' % (str(i), s.channel.name + ' (' + s.short_name + ')\n')
-            url =  s.channel.protocol + '://' + s.channel.host + ':' + str(s.channel.port) + s.channel.mount + '\n'
-            m3u.write(info)
-            m3u.write(url)
-            i += 1
-        m3u.close()
-
-    def run(self):
-        if isinstance(self.conf['deefuzz']['station'], dict):
-            # Fix wrong type data from xmltodict when one station (*)
-            nb_stations = 1
-        else:
-            nb_stations = len(self.conf['deefuzz']['station'])
-
-        # Create a Queue
-        # Not too much, otherwise, you will get memory leaks !
-        q = Queue.Queue(1)
-
-        # Create a Producer 
-        p = Producer(q)
-        p.start()
-
-        # Set the deefuzz logger
-        self.logger.write('Starting DeeFuzz v' + version)
-        self.logger.write('Using libshout version %s' % shout.version())
-
-        # Define the buffer_size
-        self.buffer_size = 65536
-        self.logger.write('Buffer size per station = ' + str(self.buffer_size))
-
-        # Init all Stations
-        self.stations = []
-        self.logger.write('Number of stations : ' + str(nb_stations))
-        for i in range(0,nb_stations):
-            if isinstance(self.conf['deefuzz']['station'], dict):
-                station = self.conf['deefuzz']['station']
-            else:
-                station = self.conf['deefuzz']['station'][i]
-            # Create a Station
-            self.stations.append(Station(station, q, self.buffer_size, self.logger))
-
-        # Create M3U playlist
-        self.logger.write('Writing M3U file to : ' + self.m3u)
-        self.set_m3u_playlist()
-        
-        # Start the Stations
-        for i in range(0,nb_stations):
-            self.stations[i].start()
-
-
-class Producer(Thread):
-    """a DeeFuzz Producer master thread"""
-
-    def __init__(self, q):
-        Thread.__init__(self)
-        self.q = q
-
-    def run(self):
-        i=0
-        q = self.q
-        while 1: 
-            q.put(i,1)
-            i+=1
-
-
-class Station(Thread):
-    """a DeeFuzz shouting station thread"""
-
-    def __init__(self, station, q, buffer_size, logger):
-        Thread.__init__(self)
-        self.station = station
-        self.q = q
-        self.buffer_size = buffer_size
-        self.logger = logger
-        self.channel = shout.Shout()
-        self.id = 999999
-        self.counter = 0
-        self.index_list = []
-        self.command = 'cat '
-        self.delay = 0
-        # Media
-        self.media_dir = self.station['media']['dir']
-        self.channel.format = self.station['media']['format']
-        self.mode_shuffle = 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']
-        self.rss_dir = self.station['media']['rss_dir']
-        self.rss_enclosure = self.station['media']['rss_enclosure']
-        # Infos
-        self.short_name = self.station['infos']['short_name']
-        self.channel.name = self.station['infos']['name']
-        self.channel.genre = self.station['infos']['genre']
-        self.channel.description = self.station['infos']['description']
-        self.channel.url = self.station['infos']['url']
-        self.rss_current_file = self.rss_dir + os.sep + self.short_name + '_' + self.channel.format + '_current.xml'
-        self.rss_playlist_file = self.rss_dir + os.sep + self.short_name + '_' + self.channel.format + '_playlist.xml'
-        self.m3u_playlist_file = self.rss_dir + os.sep + self.short_name + '.m3u'
-        self.media_url_dir = '/media/'
-        # 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 = { 'SHOUT_AI_BITRATE': self.bitrate,
-                                    'SHOUT_AI_SAMPLERATE': self.samplerate,
-                                    'SHOUT_AI_QUALITY': self.ogg_quality,
-                                    'SHOUT_AI_CHANNELS': self.voices,
-                                  }
-        self.set_playlist()
-        self.lp = len(self.playlist)
-        self.channel.open()
-        # Logging
-        self.logger.write('Opening ' + self.short_name + ' - ' + self.channel.name + \
-                ' (' + str(self.lp) + ' tracks)...')
-
-    def update_rss(self, media_list, rss_file):
-        rss_item_list = []
-        if not os.path.exists(self.rss_dir):
-            os.makedirs(self.rss_dir)
-
-        if len(media_list) == 1:
-            sub_title = '(currently playing)'
-        else:
-            sub_title = '(playlist)'
-
-        channel_subtitle = self.channel.name + ' ' + sub_title
-
-        for media in media_list:
-            media_description = '<table>'
-            for key in media.metadata.keys():
-                if media.metadata[key] != '':
-                    media_description += '<tr><td>%s:   </td><td><b>%s</b></td></tr>' % \
-                                            (key.capitalize(), media.metadata[key])
-            media_description += '<tr><td>%s:   </td><td><b>%s</b></td></tr>' % \
-                                            ('Duration', str(media.length).split('.')[0])
-            media_description += '<tr><td>%s:   </td><td><b>%s</b></td></tr>' % \
-                                            ('Bitrate', str(media.bitrate) + ' kbps')
-            media_description += '</table>'
-            media_stats = os.stat(media.media)
-            media_date = time.localtime(media_stats[8])
-            media_date = time.strftime("%a, %d %b %Y %H:%M:%S +0000", media_date)
-            date_now = str(datetime.datetime.now())
-
-            title = media.metadata['title']
-            artist = media.metadata['artist']
-            if not (title or artist):
-                song = str(media.file_title)
-            else:
-                song = artist + ' : ' + title
-
-            if self.rss_enclosure == '1':
-                media_link = self.channel.url + self.media_url_dir + media.file_name
-                media_link = media_link.decode('utf-8')
-                rss_item_list.append(PyRSS2Gen.RSSItem(
-                    title = song,
-                    link = media_link,
-                    description = media_description,
-                    enclosure = PyRSS2Gen.Enclosure(media_link, str(media.size), 'audio/mpeg'),
-                    guid = PyRSS2Gen.Guid(media_link),
-                    pubDate = media_date,)
-                    )
-            else:
-                media_link = self.channel.url
-                rss_item_list.append(PyRSS2Gen.RSSItem(
-                    title = song,
-                    link = media_link,
-                    description = media_description,
-                    guid = PyRSS2Gen.Guid(media_link),
-                    pubDate = media_date,)
-                    )
-
-        rss = PyRSS2Gen.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 set_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 '/.' in file:
-                    file_list.append(root + os.sep + file)
-        self.playlist = file_list
-
-    def get_next_media(self):
-        # Init playlist
-        if self.lp != 0:
-            self.set_playlist()
-            lp_new = len(self.playlist)
-
-            if lp_new != self.lp or self.counter == 0:
-                self.id = 0
-                self.lp = lp_new
-                self.index_list = range(0,self.lp)
-                if self.mode_shuffle == 1:
-                    # Shake it, Fuzz it !
-                    random.shuffle(self.index_list)
-                self.logger.write('Station ' + self.short_name + \
-                                 ' : generating new playlist (' + str(self.lp) + ' tracks)')
-                self.update_rss(self.media_to_objs(self.playlist), self.rss_playlist_file)
-            # Or follow...
-            else:
-                self.id = (self.id + 1) % self.lp
-
-            return self.playlist[self.index_list[self.id]]
-
-    def log_queue(self, it):
-        self.logger.write('Station ' + self.short_name + ' eated one queue step: '+str(it))
-
-    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 core_process_stream(self, media):
-        """Read media and stream data through a generator.
-        Taken from Telemeta (see http://telemeta.org)"""
-
-        command = self.command + '"' + media + '"'
-
-        proc = subprocess.Popen(command,
-                    shell = True,
-                    bufsize = self.buffer_size,
-                    stdin = subprocess.PIPE,
-                    stdout = subprocess.PIPE,
-                    close_fds = True)
-
-        # Core processing
-        while True:
-            __chunk = proc.stdout.read(self.buffer_size)
-            status = proc.poll()
-            if status != None and status != 0:
-                raise DeeFuzzStreamError('Command failure:', command, proc)
-            if not __chunk:
-                break
-            yield __chunk
-
-    def core_process_read(self, media):
-        """Read media and stream data through a generator.
-        Taken from Telemeta (see http://telemeta.org)"""
-
-        m = open(media, 'r')
-        while True:
-            __chunk = m.read(self.buffer_size)
-            if not __chunk:
-                break
-            yield __chunk
-        m.close()
-
-    def run(self):
-        q = self.q
-        while True:
-            it = q.get(1)
-            if self.lp == 0:
-                self.logger.write('Error : Station ' + self.short_name + ' have no media to stream !')
-                break
-            media = self.get_next_media()
-            self.counter += 1
-            q.task_done()
-
-            it = q.get(1)
-            if os.path.exists(media) and not os.sep+'.' in media:
-                try:
-                    self.current_media_obj = self.media_to_objs([media])
-                except:
-                    self.logger.write('Error : Station ' + self.short_name + ' : ' + media + 'not found !')
-                    break
-
-                title = self.current_media_obj[0].metadata['title']
-                artist = self.current_media_obj[0].metadata['artist']
-                if not (title or artist):
-                    song = str(self.current_media_obj[0].file_name)
-                else:
-                    song = artist + ' : ' + title
-
-                self.channel.set_metadata({'song': str(song.encode('utf-8')), 'charset': 'utf8',})
-                self.update_rss(self.current_media_obj, self.rss_current_file)
-                self.logger.write('DeeFuzzing this file on %s :  id = %s, index = %s, name = %s' \
-                    % (self.short_name, self.id, self.index_list[self.id], self.current_media_obj[0].file_name))
-
-                stream = self.core_process_read(media)
-                q.task_done()
-
-                for __chunk in stream:
-                    it = q.get(1)
-                    try:
-                        self.channel.send(__chunk)
-                        self.channel.sync()
-                        # self.logger.write('Station delay (ms) ' + self.short_name + ' : '  + str(self.channel.delay()))
-                    except:
-                        self.logger.write('ERROR : Station %s : could not send the buffer... ') % self.short_name
-                    q.task_done()
-                stream.close()
-
-        self.channel.close()
-
-
-def main():
-    if len(sys.argv) == 2:
-        d = DeeFuzz(sys.argv[1])
-        d.start()
-    else:
-        text = prog_info()
-        sys.exit(text)
-
-if __name__ == '__main__':
-    main()
diff --git a/deefuzzer-deamon.sh b/deefuzzer-deamon.sh
new file mode 100755 (executable)
index 0000000..4e2f9a6
--- /dev/null
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+conf_file=$1
+log_file=/tmp/deefuzzer.log
+
+set -e
+ulimit -c unlimited
+while true; do
+  deefuzzer $1 > /dev/null
+  sleep 3
+done
diff --git a/deefuzzer.py b/deefuzzer.py
new file mode 100755 (executable)
index 0000000..e5902e0
--- /dev/null
@@ -0,0 +1,457 @@
+#!/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 sys
+import time
+import datetime
+import string
+import random
+import Queue
+import shout
+import subprocess
+from threading import Thread
+from tools import *
+
+version = '0.3.0'
+year = datetime.datetime.now().strftime("%Y")
+
+
+def prog_info():
+        desc = '\n deefuzzer : easy and light streaming tool\n'
+        ver = ' version : %s \n\n' % (version)
+        info = """ 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/d-fuzz/DeeFuzzerLicense
+
+ depends : python, python-xml, python-shout, libshout3, icecast2
+ recommends : python-mutagen
+ provides : python-shout
+
+ 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
+        """ % (year)
+        text = desc + ver + info
+        return text
+
+
+class DeeFuzzerError:
+    """The DeeFuzzer main error class"""
+    def __init__(self, message):
+        self.message = message
+    def __str__(self):
+        return 'DeeFuzzer Error : ' + self.message
+
+
+class DeeFuzzerStreamError:
+    """The DeeFuzzer stream error class"""
+    def __init__(self, message, command, subprocess):
+        self.message = message
+        self.command = str(command)
+        self.subprocess = subprocess
+
+    def __str__(self):
+        if self.subprocess.stderr != None:
+            error = self.subprocess.stderr.read()
+        else:
+            error = ''
+        return "%s ; command: %s; error: %s" % (self.message,
+                                                self.command,
+                                                error)
+
+
+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'
+
+    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')
+        i = 1
+        m3u.write('#EXTM3U\n')
+        for s in self.stations:
+            info = '#EXTINF:%s,%s' % (str(i), s.channel.name + ' (' + s.short_name + ')\n')
+            url =  s.channel.protocol + '://' + s.channel.host + ':' + str(s.channel.port) + s.channel.mount + '\n'
+            m3u.write(info)
+            m3u.write(url)
+            i += 1
+        m3u.close()
+
+    def run(self):
+        if isinstance(self.conf['deefuzzer']['station'], dict):
+            # Fix wrong type data from xmltodict when one station (*)
+            nb_stations = 1
+        else:
+            nb_stations = len(self.conf['deefuzzer']['station'])
+
+        # Create a Queue
+        # Not too much, otherwise, you will get memory leaks !
+        q = Queue.Queue(1)
+
+        # Create a Producer 
+        p = Producer(q)
+        p.start()
+
+        # Set the deefuzzer logger
+        self.logger.write('Starting DeeFuzzer v' + version)
+        self.logger.write('Using libshout version %s' % shout.version())
+
+        # Define the buffer_size
+        self.buffer_size = 65536
+        self.logger.write('Buffer size per station = ' + str(self.buffer_size))
+
+        # Init all Stations
+        self.stations = []
+        self.logger.write('Number of stations : ' + str(nb_stations))
+        for i in range(0,nb_stations):
+            if isinstance(self.conf['deefuzzer']['station'], dict):
+                station = self.conf['deefuzzer']['station']
+            else:
+                station = self.conf['deefuzzer']['station'][i]
+            # Create a Station
+            self.stations.append(Station(station, q, self.buffer_size, self.logger))
+
+        # Create M3U playlist
+        self.logger.write('Writing M3U file to : ' + self.m3u)
+        self.set_m3u_playlist()
+        
+        # Start the Stations
+        for i in range(0,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 1: 
+            q.put(i,1)
+            i+=1
+
+
+class Station(Thread):
+    """a DeeFuzzer shouting station thread"""
+
+    def __init__(self, station, q, buffer_size, logger):
+        Thread.__init__(self)
+        self.station = station
+        self.q = q
+        self.buffer_size = buffer_size
+        self.logger = logger
+        self.channel = shout.Shout()
+        self.id = 999999
+        self.counter = 0
+        self.index_list = []
+        self.command = 'cat '
+        self.delay = 0
+        # Media
+        self.media_dir = self.station['media']['dir']
+        self.channel.format = self.station['media']['format']
+        self.mode_shuffle = 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']
+        self.rss_dir = self.station['media']['rss_dir']
+        self.rss_enclosure = self.station['media']['rss_enclosure']
+        # Infos
+        self.short_name = self.station['infos']['short_name']
+        self.channel.name = self.station['infos']['name']
+        self.channel.genre = self.station['infos']['genre']
+        self.channel.description = self.station['infos']['description']
+        self.channel.url = self.station['infos']['url']
+        self.rss_current_file = self.rss_dir + os.sep + self.short_name + '_' + self.channel.format + '_current.xml'
+        self.rss_playlist_file = self.rss_dir + os.sep + self.short_name + '_' + self.channel.format + '_playlist.xml'
+        self.m3u_playlist_file = self.rss_dir + os.sep + self.short_name + '.m3u'
+        self.media_url_dir = '/media/'
+        # 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 = { 'SHOUT_AI_BITRATE': self.bitrate,
+                                    'SHOUT_AI_SAMPLERATE': self.samplerate,
+                                    'SHOUT_AI_QUALITY': self.ogg_quality,
+                                    'SHOUT_AI_CHANNELS': self.voices,
+                                  }
+        self.set_playlist()
+        self.lp = len(self.playlist)
+        self.channel.open()
+        # Logging
+        self.logger.write('Opening ' + self.short_name + ' - ' + self.channel.name + \
+                ' (' + str(self.lp) + ' tracks)...')
+
+    def update_rss(self, media_list, rss_file):
+        rss_item_list = []
+        if not os.path.exists(self.rss_dir):
+            os.makedirs(self.rss_dir)
+
+        if len(media_list) == 1:
+            sub_title = '(currently playing)'
+        else:
+            sub_title = '(playlist)'
+
+        channel_subtitle = self.channel.name + ' ' + sub_title
+
+        for media in media_list:
+            media_description = '<table>'
+            for key in media.metadata.keys():
+                if media.metadata[key] != '':
+                    media_description += '<tr><td>%s:   </td><td><b>%s</b></td></tr>' % \
+                                            (key.capitalize(), media.metadata[key])
+            media_description += '<tr><td>%s:   </td><td><b>%s</b></td></tr>' % \
+                                            ('Duration', str(media.length).split('.')[0])
+            media_description += '<tr><td>%s:   </td><td><b>%s</b></td></tr>' % \
+                                            ('Bitrate', str(media.bitrate) + ' kbps')
+            media_description += '</table>'
+            media_stats = os.stat(media.media)
+            media_date = time.localtime(media_stats[8])
+            media_date = time.strftime("%a, %d %b %Y %H:%M:%S +0000", media_date)
+            date_now = str(datetime.datetime.now())
+
+            title = media.metadata['title']
+            artist = media.metadata['artist']
+            if not (title or artist):
+                song = str(media.file_title)
+            else:
+                song = artist + ' : ' + title
+
+            if self.rss_enclosure == '1':
+                media_link = self.channel.url + self.media_url_dir + media.file_name
+                media_link = media_link.decode('utf-8')
+                rss_item_list.append(PyRSS2Gen.RSSItem(
+                    title = song,
+                    link = media_link,
+                    description = media_description,
+                    enclosure = PyRSS2Gen.Enclosure(media_link, str(media.size), 'audio/mpeg'),
+                    guid = PyRSS2Gen.Guid(media_link),
+                    pubDate = media_date,)
+                    )
+            else:
+                media_link = self.channel.url
+                rss_item_list.append(PyRSS2Gen.RSSItem(
+                    title = song,
+                    link = media_link,
+                    description = media_description,
+                    guid = PyRSS2Gen.Guid(media_link),
+                    pubDate = media_date,)
+                    )
+
+        rss = PyRSS2Gen.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 set_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 '/.' in file:
+                    file_list.append(root + os.sep + file)
+        self.playlist = file_list
+
+    def get_next_media(self):
+        # Init playlist
+        if self.lp != 0:
+            self.set_playlist()
+            lp_new = len(self.playlist)
+
+            if lp_new != self.lp or self.counter == 0:
+                self.id = 0
+                self.lp = lp_new
+                self.index_list = range(0,self.lp)
+                if self.mode_shuffle == 1:
+                    # Shake it, Fuzz it !
+                    random.shuffle(self.index_list)
+                self.logger.write('Station ' + self.short_name + \
+                                 ' : generating new playlist (' + str(self.lp) + ' tracks)')
+                self.update_rss(self.media_to_objs(self.playlist), self.rss_playlist_file)
+            # Or follow...
+            else:
+                self.id = (self.id + 1) % self.lp
+
+            return self.playlist[self.index_list[self.id]]
+
+    def log_queue(self, it):
+        self.logger.write('Station ' + self.short_name + ' eated one queue step: '+str(it))
+
+    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 core_process_stream(self, media):
+        """Read media and stream data through a generator.
+        Taken from Telemeta (see http://telemeta.org)"""
+
+        command = self.command + '"' + media + '"'
+
+        proc = subprocess.Popen(command,
+                    shell = True,
+                    bufsize = self.buffer_size,
+                    stdin = subprocess.PIPE,
+                    stdout = subprocess.PIPE,
+                    close_fds = True)
+
+        # Core processing
+        while True:
+            __chunk = proc.stdout.read(self.buffer_size)
+            status = proc.poll()
+            if status != None and status != 0:
+                raise DeeFuzzerStreamError('Command failure:', command, proc)
+            if not __chunk:
+                break
+            yield __chunk
+
+    def core_process_read(self, media):
+        """Read media and stream data through a generator.
+        Taken from Telemeta (see http://telemeta.org)"""
+
+        m = open(media, 'r')
+        while True:
+            __chunk = m.read(self.buffer_size)
+            if not __chunk:
+                break
+            yield __chunk
+        m.close()
+
+    def run(self):
+        q = self.q
+        while True:
+            it = q.get(1)
+            if self.lp == 0:
+                self.logger.write('Error : Station ' + self.short_name + ' have no media to stream !')
+                break
+            media = self.get_next_media()
+            self.counter += 1
+            q.task_done()
+
+            it = q.get(1)
+            if os.path.exists(media) and not os.sep+'.' in media:
+                try:
+                    self.current_media_obj = self.media_to_objs([media])
+                except:
+                    self.logger.write('Error : Station ' + self.short_name + ' : ' + media + 'not found !')
+                    break
+
+                title = self.current_media_obj[0].metadata['title']
+                artist = self.current_media_obj[0].metadata['artist']
+                if not (title or artist):
+                    song = str(self.current_media_obj[0].file_name)
+                else:
+                    song = artist + ' : ' + title
+
+                self.channel.set_metadata({'song': str(song.encode('utf-8')), 'charset': 'utf8',})
+                self.update_rss(self.current_media_obj, self.rss_current_file)
+                self.logger.write('DeeFuzzing this file on %s :  id = %s, index = %s, name = %s' \
+                    % (self.short_name, self.id, self.index_list[self.id], self.current_media_obj[0].file_name))
+
+                stream = self.core_process_read(media)
+                q.task_done()
+
+                for __chunk in stream:
+                    it = q.get(1)
+                    try:
+                        self.channel.send(__chunk)
+                        self.channel.sync()
+                        # self.logger.write('Station delay (ms) ' + self.short_name + ' : '  + str(self.channel.delay()))
+                    except:
+                        self.logger.write('ERROR : Station %s : could not send the buffer... ') % self.short_name
+                    q.task_done()
+                stream.close()
+
+        self.channel.close()
+
+
+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()
index 51fb68261ad8c2e2564e4116b470a1845441bc42..a799e16a80802b7b46af7976271b2bde1d34fcb6 100644 (file)
@@ -1,4 +1,4 @@
-<deefuzz>
+<deefuzzer>
     <log>/tmp/deefuzz.log</log>
     <m3u>/tmp/deefuzz.m3u</m3u>
     <station>
@@ -55,5 +55,5 @@
             <rss_enclosure>1</rss_enclosure>
         </media>
     </station>
-</deefuzz>
+</deefuzzer>
 
index d9b569b750ce37f725071f7168eb9c36b7a32717..c2bb82aafab0f1ead8e838a901e84374a0b6d48c 100644 (file)
@@ -1,4 +1,4 @@
-<deefuzz>
+<deefuzzer>
     <log>/tmp/deefuzz.log</log>
     <station>
         <infos>
@@ -48,5 +48,5 @@
             <shuffle>1</shuffle>
         </media>
     </station>
-</deefuzz>
+</deefuzzer>
 
index e8373e4ab47d3b146338c4d46ba778be33f2090d..410b96a5a42051f1241ae42f4be93bba7c02085a 100644 (file)
@@ -1,4 +1,4 @@
-<deefuzz>
+<deefuzzer>
     <log>/tmp/deefuzz.log</log>
     <station>
         <infos>
             <rss_enclosure>0</rss_enclosure>
         </media>
     </station>
-</deefuzz>
+</deefuzzer>