├── debian ├── compat ├── docs ├── dirs ├── README.Debian ├── rules ├── changelog ├── control ├── manpage.1.ex ├── copyright ├── manpage.xml.ex └── manpage.sgml.ex ├── deefuzzer ├── version.py ├── __init__.py ├── tools │ ├── xml2yaml.py │ ├── __init__.py │ ├── xmltodict3.py │ ├── xmltodict.py │ ├── osc.py │ ├── logger.py │ ├── twitt.py │ ├── streamer.py │ ├── webm.py │ ├── get_access_token.py │ ├── utils.py │ ├── mp3.py │ ├── mediabase.py │ ├── ogg.py │ ├── PyRSS2Gen.py │ └── xmltodict2.py ├── recorder.py ├── relay.py ├── streamer.py ├── player.py └── core.py ├── doc └── img │ ├── logo_deefuzzer.png │ ├── logo_deefuzzer_wh.png │ └── logo_deefuzzer_notxt.png ├── scripts ├── dev │ ├── pypi.sh │ ├── make_doc.sh │ └── xml2yaml.py ├── osc │ ├── osc_player_next.py │ ├── osc_player_fast.py │ ├── osc_player_slow.py │ ├── osc_record_start.py │ ├── osc_record_stop.py │ ├── osc_jingles_start.py │ ├── osc_jingles_stop.py │ ├── osc_relay_start.py │ ├── osc_relay_stop.py │ ├── osc_twitter_start.py │ └── osc_twitter_stop.py ├── deefuzzer └── etc │ ├── default │ └── deefuzzer │ └── init.d │ └── deefuzzer ├── MANIFEST.in ├── .gitignore ├── .travis.yml ├── NEWS.rst ├── example ├── stationconfig_doc.xml ├── deefuzzer.yaml ├── deefuzzer_telecaster_test.yaml ├── deefuzzer_webm.json ├── deefuzzer_webm_relay.json ├── deefuzzer_webm_file.json ├── deefuzzer.xml ├── deefuzzer_test.xml ├── deefuzzer_multiple.yaml ├── deefuzzer_webm_relay_multiple.yaml ├── deefuzzer.json └── deefuzzer_doc.xml ├── setup.py ├── README.rst └── LICENSE.txt /debian/compat: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README 2 | -------------------------------------------------------------------------------- /debian/dirs: -------------------------------------------------------------------------------- 1 | usr/bin 2 | usr/share 3 | 4 | -------------------------------------------------------------------------------- /deefuzzer/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.9.2' 2 | -------------------------------------------------------------------------------- /deefuzzer/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import * 2 | from .station import * 3 | from .tools import * 4 | -------------------------------------------------------------------------------- /doc/img/logo_deefuzzer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomguy/DeeFuzzer/HEAD/doc/img/logo_deefuzzer.png -------------------------------------------------------------------------------- /scripts/dev/pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python setup.py register 4 | python setup.py sdist upload 5 | 6 | -------------------------------------------------------------------------------- /doc/img/logo_deefuzzer_wh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomguy/DeeFuzzer/HEAD/doc/img/logo_deefuzzer_wh.png -------------------------------------------------------------------------------- /doc/img/logo_deefuzzer_notxt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yomguy/DeeFuzzer/HEAD/doc/img/logo_deefuzzer_notxt.png -------------------------------------------------------------------------------- /debian/README.Debian: -------------------------------------------------------------------------------- 1 | d-fuzz for Debian 2 | ----------------- 3 | 4 | 5 | 6 | -- G. Pellerin Sun, 09 Dec 2007 21:50:43 +0100 7 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | include /usr/share/cdbs/1/rules/debhelper.mk 4 | include /usr/share/cdbs/1/class/makefile.mk 5 | include /usr/share/cdbs/1/class/python-distutils.mk 6 | 7 | # Add here any variable or target overrides you need. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | include *.md 4 | exclude *.cfg 5 | recursive-include doc * 6 | recursive-include example * 7 | recursive-include scripts * 8 | global-exclude *.pyc 9 | prune dist 10 | prune build 11 | recursive-exclude debian * 12 | -------------------------------------------------------------------------------- /scripts/dev/make_doc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # needs epydoc 3 | 4 | app="deefuzzer" 5 | dir=/home/$USER/dev/$app/doc/ 6 | server="doc.parisson.com" 7 | 8 | epydoc -n $app -u https://github.com/yomguy/DeeFuzzer -o $dir $app/ 9 | rsync -a $dir $server:/var/www/files/doc/$app/ 10 | 11 | -------------------------------------------------------------------------------- /deefuzzer/tools/xml2yaml.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import yaml 4 | from deefuzzer.core import DeeFuzzer 5 | 6 | 7 | path = sys.argv[-1] 8 | d = DeeFuzzer(path) 9 | name, ext = os.path.splitext(path) 10 | y = open(name + '.yaml', 'w') 11 | yaml.dump(d.conf, y) 12 | y.close() 13 | -------------------------------------------------------------------------------- /deefuzzer/tools/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .xmltodict3 import * 3 | from .PyRSS2Gen import * 4 | from .mediabase import * 5 | from .mp3 import * 6 | from .ogg import * 7 | from .webm import * 8 | from .logger import * 9 | from .osc import * 10 | from .twitt import * 11 | from .utils import * 12 | -------------------------------------------------------------------------------- /scripts/osc/osc_player_next.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import liblo 5 | import sys 6 | 7 | # send all messages to port 1234 on the local machine 8 | try: 9 | target = liblo.Address(16001) 10 | except liblo.AddressError as err: 11 | sys.exit(err) 12 | 13 | # send message "/foo/message1" with int, float and string arguments 14 | liblo.send(target, "/media/next", 1) 15 | -------------------------------------------------------------------------------- /scripts/osc/osc_player_fast.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import liblo 5 | import sys 6 | 7 | # send all messages to port 1234 on the local machine 8 | try: 9 | target = liblo.Address(16001) 10 | except liblo.AddressError as err: 11 | print(str(err)) 12 | sys.exit() 13 | 14 | # send message "/foo/message1" with int, float and string arguments 15 | liblo.send(target, "/player", 1) 16 | -------------------------------------------------------------------------------- /scripts/osc/osc_player_slow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import liblo 5 | import sys 6 | 7 | # send all messages to port 1234 on the local machine 8 | try: 9 | target = liblo.Address(16001) 10 | except liblo.AddressError as err: 11 | print(str(err)) 12 | sys.exit() 13 | 14 | # send message "/foo/message1" with int, float and string arguments 15 | liblo.send(target, "/player", 0) 16 | -------------------------------------------------------------------------------- /scripts/osc/osc_record_start.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import liblo 5 | import sys 6 | 7 | # send all messages to port 1234 on the local machine 8 | try: 9 | target = liblo.Address(16001) 10 | except liblo.AddressError as err: 11 | print(str(err)) 12 | sys.exit() 13 | 14 | # send message "/foo/message1" with int, float and string arguments 15 | liblo.send(target, "/record", 1) 16 | -------------------------------------------------------------------------------- /scripts/osc/osc_record_stop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import liblo 5 | import sys 6 | 7 | # send all messages to port 1234 on the local machine 8 | try: 9 | target = liblo.Address(16001) 10 | except liblo.AddressError as err: 11 | print(str(err)) 12 | sys.exit() 13 | 14 | # send message "/foo/message1" with int, float and string arguments 15 | liblo.send(target, "/record", 0) 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *~ 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | eggs 10 | parts 11 | bin 12 | var 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | 17 | # Installer logs 18 | pip-log.txt 19 | 20 | # Unit test / coverage reports 21 | .coverage 22 | .tox 23 | 24 | #Translations 25 | *.mo 26 | 27 | #Mr Developer 28 | .mr.developer.cfg 29 | 30 | #PyCharm IDE 31 | .idea 32 | 33 | #vim 34 | *.swp 35 | -------------------------------------------------------------------------------- /scripts/osc/osc_jingles_start.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import liblo 5 | import sys 6 | 7 | # send all messages to port 1234 on the local machine 8 | try: 9 | target = liblo.Address(16001) 10 | except liblo.AddressError as err: 11 | print(str(err)) 12 | sys.exit() 13 | 14 | # send message "/foo/message1" with int, float and string arguments 15 | liblo.send(target, "/jingles", 1) 16 | -------------------------------------------------------------------------------- /scripts/osc/osc_jingles_stop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import liblo 5 | import sys 6 | 7 | # send all messages to port 1234 on the local machine 8 | try: 9 | target = liblo.Address(16001) 10 | except liblo.AddressError as err: 11 | print(str(err)) 12 | sys.exit() 13 | 14 | # send message "/foo/message1" with int, float and string arguments 15 | liblo.send(target, "/jingles", 0) 16 | -------------------------------------------------------------------------------- /scripts/osc/osc_relay_start.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import liblo 5 | import sys 6 | 7 | # send all messages to port 1234 on the local machine 8 | try: 9 | target = liblo.Address(16001) 10 | except liblo.AddressError as err: 11 | print(str(err)) 12 | sys.exit() 13 | 14 | # send message "/foo/message1" with int, float and string arguments 15 | liblo.send(target, "/media/relay", 1) 16 | -------------------------------------------------------------------------------- /scripts/osc/osc_relay_stop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import liblo 5 | import sys 6 | 7 | # send all messages to port 1234 on the local machine 8 | try: 9 | target = liblo.Address(16001) 10 | except liblo.AddressError as err: 11 | print(str(err)) 12 | sys.exit() 13 | 14 | # send message "/foo/message1" with int, float and string arguments 15 | liblo.send(target, "/media/relay", 0) 16 | -------------------------------------------------------------------------------- /scripts/osc/osc_twitter_start.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import liblo 5 | import sys 6 | 7 | # send all messages to port 1234 on the local machine 8 | try: 9 | target = liblo.Address(16001) 10 | except liblo.AddressError as err: 11 | print(str(err)) 12 | sys.exit() 13 | 14 | # send message "/foo/message1" with int, float and string arguments 15 | liblo.send(target, "/twitter", 1) 16 | -------------------------------------------------------------------------------- /scripts/osc/osc_twitter_stop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import liblo 5 | import sys 6 | 7 | # send all messages to port 1234 on the local machine 8 | try: 9 | target = liblo.Address(16001) 10 | except liblo.AddressError as err: 11 | print(str(err)) 12 | sys.exit() 13 | 14 | # send message "/foo/message1" with int, float and string arguments 15 | liblo.send(target, "/twitter", 0) 16 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | d-fuzz (0.3.3-1) unstable; urgency=low 2 | 3 | * add twitter api 4 | * cleanup RSS and song metadata 5 | 6 | -- momo Tue, 27 Oct 2009 13:44:06 +0100 7 | 8 | d-fuzz (0.3.0-1) unstable; urgency=low 9 | 10 | * new and much upgraded DeeFuzz 11 | 12 | -- Guillaume Pellerin Fri, 17 Apr 2009 09:48:25 +0200 13 | 14 | d-fuzz (0.2-1) unstable; urgency=low 15 | 16 | * Initial release 17 | 18 | -- G. Pellerin Sun, 09 Dec 2007 21:50:43 +0100 19 | 20 | -------------------------------------------------------------------------------- /scripts/deefuzzer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import argparse 4 | import deefuzzer 5 | 6 | from deefuzzer.version import __version__ 7 | 8 | def main(): 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('-v', '--version', help='show version', action="version", version='%(prog)s ' + __version__) 11 | parser.add_argument('config_file', type=str, help='config file (YAML, XML or JSON)') 12 | args = parser.parse_args() 13 | 14 | d = deefuzzer.core.DeeFuzzer(args) 15 | d.start() 16 | 17 | if __name__ == '__main__': 18 | main() 19 | 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | matrix: 4 | include: 5 | - python: 3.7 6 | os: linux 7 | compiler: gcc 8 | 9 | virtualenv: 10 | system_site_packages: true 11 | 12 | # command to prepare the system to install prerequisites or dependencies 13 | before_install: 14 | - sudo apt-get install -qq python-setuptools 15 | - sudo apt-get install -qq python-pip python-dev libshout3-dev python-liblo python-mutagen python-pycurl liblo-dev libshout3-dev librtmp-dev python-yaml libcurl4-openssl-dev python-mutagen libmariadbclient-dev 16 | 17 | # command to run tests 18 | script: 19 | - "python setup.py install" 20 | # - "python tests/testcomponent.py" 21 | -------------------------------------------------------------------------------- /scripts/etc/default/deefuzzer: -------------------------------------------------------------------------------- 1 | # Defaults for deefuzzer initscript 2 | # sourced by /etc/init.d/deefuzzer 3 | # installed at /etc/default/deefuzzer by the maintainer scripts 4 | 5 | # 6 | # This is a POSIX shell fragment 7 | # 8 | 9 | # Full path to the server configuration file 10 | CONFIGFILE="/etc/deefuzzer/deefuzzer.xml" 11 | 12 | # Full path to the process ID file 13 | PIDFILE="/var/log/deefuzzer/deefuzzer.pid" 14 | 15 | # Name or ID of the user and group the daemon should run under 16 | USERID=deefuzzer 17 | GROUPID=staff 18 | 19 | # Edit /etc/deefuzzer.xml to match your environment. 20 | # Change this to true when done to enable the init.d script 21 | ENABLE=false 22 | 23 | # Uncomment below to make the service startup more verbose 24 | QUIET="--verbose" 25 | 26 | -------------------------------------------------------------------------------- /NEWS.rst: -------------------------------------------------------------------------------- 1 | Old news 2 | ======== 3 | 4 | See README.rst for last news. 5 | 6 | 0.6.4 7 | 8 | * Fix install bug again (add main script to install), sorry :( 9 | * Reduce streaming buffer length 10 | 11 | 0.6.3 12 | 13 | * Fix install bug 14 | * Setup rewritten 15 | * Fix MANIFEST 16 | 17 | 0.6.2 18 | 19 | * No new functions but bugfixes (including a serious one during install from pypi) 20 | * Definitely moved the project to `GitHub `_ 21 | * Update various README details 22 | * update API doc: http://files.parisson.com/doc/deefuzzer/ 23 | 24 | 0.6.1 25 | 26 | * New HTTP steamer based on pycurl to handle streaming over stream-m servers (WebM streaming) 27 | see http://code.google.com/p/stream-m/ 28 | * Live webm relaying works good, webm playlist reading NEED testing 29 | * New parameter ('icecast or 'stream-m') 30 | -------------------------------------------------------------------------------- /deefuzzer/tools/xmltodict3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # taken from https://stackoverflow.com/questions/2148119/how-to-convert-an-xml-string-to-a-dictionary 3 | # thanks to K3---rnc 4 | 5 | 6 | from collections import defaultdict 7 | 8 | def etree_to_dict(t): 9 | d = {t.tag: {} if t.attrib else None} 10 | children = list(t) 11 | if children: 12 | dd = defaultdict(list) 13 | for dc in map(etree_to_dict, children): 14 | for k, v in dc.items(): 15 | dd[k].append(v) 16 | d = {t.tag: {k:v[0] if len(v) == 1 else v for k, v in dd.items()}} 17 | if t.attrib: 18 | d[t.tag].update(('@' + k, v) for k, v in t.attrib.items()) 19 | if t.text: 20 | text = t.text.strip() 21 | if children or t.attrib: 22 | if text: 23 | d[t.tag]['#text'] = text 24 | else: 25 | d[t.tag] = text 26 | return d -------------------------------------------------------------------------------- /deefuzzer/tools/xmltodict.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import xml.dom.minidom 4 | 5 | 6 | def haschilds(dom): 7 | # Checks whether an element has any childs 8 | # containing real tags opposed to just text. 9 | for childnode in dom.childNodes: 10 | if childnode.nodeName != "#text" and childnode.nodeName != "#cdata-section": 11 | return True 12 | return False 13 | 14 | 15 | def indexchilds(dom, enc): 16 | childsdict = dict() 17 | for childnode in dom.childNodes: 18 | name = childnode.nodeName.encode(enc) 19 | if name == "#text" or name == "#cdata-section": 20 | # ignore whitespaces 21 | continue 22 | if haschilds(childnode): 23 | v = indexchilds(childnode, enc) 24 | else: 25 | v = childnode.childNodes[0].nodeValue.encode(enc) 26 | if name in childsdict: 27 | if isinstance(childsdict[name], dict): 28 | # there is multiple instances of this node - convert to list 29 | childsdict[name] = [childsdict[name]] 30 | childsdict[name].append(v) 31 | else: 32 | childsdict[name] = v 33 | return childsdict 34 | 35 | 36 | def xmltodict(data, enc=None): 37 | dom = xml.dom.minidom.parseString(data.strip()) 38 | return indexchilds(dom, enc) 39 | 40 | 41 | -------------------------------------------------------------------------------- /example/stationconfig_doc.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | My personal best funky playlist ever! 7 | My best funky station 8 | My_station 9 | http://parisson.com 10 | Various Funk Groove 11 | 12 | 13 | /path/to/jingles 14 | 0 15 | 1 16 | 17 | 18 | 96 19 | /path/to/mp3/or/m3u 20 | mp3 21 | 4 22 | 48000 23 | 0 24 | 2 25 | 26 | 27 | 127.0.0.1 28 | monitor 29 | 8000 30 | 0 31 | icecast_source_password 32 | icecast 33 | 1 34 | 35 | 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup and build script for the library.""" 5 | 6 | from setuptools import setup, find_packages 7 | 8 | CLASSIFIERS = [ 9 | 'Programming Language :: Python :: 3', 10 | 'Programming Language :: Python :: 3.11', 11 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 12 | 'Topic :: Multimedia :: Sound/Audio', 13 | 'Topic :: Multimedia :: Sound/Audio :: Players', 14 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 15 | ] 16 | 17 | setup( 18 | name="DeeFuzzer", 19 | url="http://github.com/yomguy/DeeFuzzer", 20 | description="open, light and instant media streaming tool", 21 | long_description=open('README.rst').read(), 22 | author="Guillaume Pellerin", 23 | author_email="yomguy@parisson.com", 24 | version='0.9.2', 25 | install_requires=[ 26 | 'setuptools', 27 | 'wheel', 28 | 'python-shout==0.2.8', 29 | 'python-twitter==3.5', 30 | 'mutagen==1.45.1', 31 | 'pyliblo3==0.16.4', 32 | 'pycurl==7.45.6', 33 | 'pyyaml==6.0.2', 34 | 'mysqlclient==2.0.3', 35 | ], 36 | platforms=['OS Independent'], 37 | license='GPL v3', 38 | scripts=['scripts/deefuzzer'], 39 | classifiers=CLASSIFIERS, 40 | packages=find_packages(), 41 | include_package_data=True, 42 | zip_safe=False, 43 | python_requires='>=3', 44 | ) 45 | -------------------------------------------------------------------------------- /scripts/dev/xml2yaml.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2013 Guillaume Pellerin 5 | 6 | # 7 | 8 | # This file is part of deefuzzer 9 | 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | 25 | import sys 26 | from deefuzzer.tools.xmltodict import * 27 | 28 | 29 | class XML2Various(object): 30 | 31 | def __init__(self, xml_str): 32 | self.dict = xmltodict(xml_str, 'utf-8') 33 | 34 | def to_yaml(self): 35 | import yaml 36 | return yaml.dump(self.dict) 37 | 38 | 39 | if __name__ == '__main__': 40 | xml_file = open(sys.argv[-2], 'r') 41 | yaml_file = open(sys.argv[-1], 'w') 42 | yaml_file.write(XML2Various(xml_file.read()).to_yaml()) 43 | xml_file.close() 44 | yaml_file.close() 45 | 46 | 47 | -------------------------------------------------------------------------------- /deefuzzer/tools/osc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2006-2009 Guillaume Pellerin 5 | 6 | # 7 | 8 | # This file is part of deefuzzer 9 | 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | from threading import Thread 25 | 26 | 27 | class OSCController(Thread): 28 | def __init__(self, port): 29 | Thread.__init__(self) 30 | import liblo 31 | 32 | self.port = port 33 | try: 34 | self.server = liblo.Server(self.port) 35 | except liblo.ServerError as err: 36 | print(str(err)) 37 | 38 | def add_method(self, path, type, method): 39 | self.server.add_method(path, type, method) 40 | 41 | def run(self): 42 | while True: 43 | self.server.recv(100) 44 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: deefuzz 2 | Section: sound 3 | Priority: optional 4 | Maintainer: Guillaume Pellerin 5 | Build-Depends: debhelper (>= 5), python 6 | Standards-Version: 3.7.2 7 | 8 | Package: deefuzzer 9 | Architecture: any 10 | Depends: ${shlibs:Depends}, ${misc:Depends}, icecast2, python-xml, python-shout, libshout3, python-mutagen 11 | Recommends: python-twitter, python-tinyurl, python-liblo | pyliblo (>= 0.26) 12 | Description: easy and light media streaming tool 13 | 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: 14 | * MP3 and OGG (audio & video) file streaming over internet (Icecast) 15 | * Full metadata encapsulation and management 16 | * RSS podcast generator (current tracks and playlists) 17 | * M3U playlist generator 18 | * Recursive, random (shuffled) or pre-defined playlists 19 | * Multi-threaded architecture : multiple station streaming with one config file 20 | * Auto Twitter posting of the current playing tracks 21 | * Jingling between main tracks 22 | * OSC controller : control the main functions from a distant terminal 23 | * Station relaying : stream other stations like *LIVE* sessions ! 24 | * Very light and optimized streaming process 25 | See http://svn.parisson.org/deefuzzer/ or http://parisson.com for more info. 26 | -------------------------------------------------------------------------------- /deefuzzer/recorder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2006-2009 Guillaume Pellerin 5 | 6 | # 7 | 8 | # This file is part of deefuzzer 9 | 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | import os 25 | 26 | 27 | class Recorder: 28 | """A file streaming iterator""" 29 | 30 | def __init__(self, path): 31 | self.path = path 32 | self.recording = True 33 | 34 | def open(self, filename): 35 | self.filename = filename 36 | self.media = open(self.path + os.sep + self.filename, 'wb') 37 | 38 | def write(self, chunk): 39 | try: 40 | if self.recording: 41 | self.media.write(chunk) 42 | self.media.flush() 43 | except: 44 | pass 45 | 46 | def close(self): 47 | self.media.close() 48 | -------------------------------------------------------------------------------- /deefuzzer/tools/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | from threading import Thread 6 | 7 | 8 | class Logger: 9 | """A logging object""" 10 | 11 | def __init__(self, filepath): 12 | self.logger = logging.getLogger('myapp') 13 | self.hdlr = logging.FileHandler(filepath) 14 | self.formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 15 | self.hdlr.setFormatter(self.formatter) 16 | self.logger.addHandler(self.hdlr) 17 | self.logger.setLevel(logging.INFO) 18 | 19 | def write_info(self, message): 20 | self.logger.info(message) 21 | 22 | def write_error(self, message): 23 | self.logger.error(message) 24 | 25 | 26 | class QueueLogger(Thread): 27 | """A queue-based logging object""" 28 | 29 | def __init__(self, filepath, q): 30 | Thread.__init__(self) 31 | self.logger = Logger(filepath) 32 | self.q = q 33 | 34 | def run(self): 35 | while True: 36 | try: 37 | msg = self.q.get(1) 38 | if not isinstance(msg, dict): 39 | self.logger.write_error(str(msg)) 40 | else: 41 | if 'msg' not in msg: 42 | continue 43 | 44 | if 'level' in msg: 45 | if msg['level'] == 'info': 46 | self.logger.write_info(msg['msg']) 47 | else: 48 | self.logger.write_error(msg['msg']) 49 | else: 50 | self.logger.write_error(msg['msg']) 51 | except: 52 | pass 53 | -------------------------------------------------------------------------------- /example/deefuzzer.yaml: -------------------------------------------------------------------------------- 1 | deefuzzer: 2 | log: /path/to/station.log 3 | m3u: /path/to/station.m3u 4 | 5 | stationdefaults: 6 | control: {mode: 0, 7 | port: 16001} 8 | 9 | jingles: {dir: /path/to/jingles, 10 | mode: 0, 11 | shuffle: 1} 12 | 13 | station: 14 | control: {mode: 0, 15 | port: 16001} 16 | 17 | infos: {description: 'My personal best funky playlist ever!', 18 | genre: 'Various Funk Groove', 19 | name: 'My best funky station', 20 | short_name: "My_station", 21 | url: 'http://parisson.com'} 22 | 23 | jingles: {dir: /path/to/jingles, 24 | mode: 0, 25 | shuffle: 1} 26 | 27 | media: {bitrate: 96, 28 | source: /path/to/mp3/or/m3u, 29 | format: mp3, 30 | ogg_quality: 4, 31 | samplerate: 48000, 32 | shuffle: 0, 33 | voices: '2'} 34 | 35 | record: {dir: /path/to/archives, 36 | mode: 0} 37 | 38 | relay: {author: Unknown, 39 | mode: 0, 40 | url: 'http://127.0.0.1:8000/telecaster_live.mp3'} 41 | 42 | feeds: {mode: 1, rss: 1, json: 0, playlist: 1, 43 | dir: /var/www/rss, 44 | enclosure: 0, 45 | media_url: 'http://localhost/media/'} 46 | 47 | server: {host: 127.0.0.1, 48 | mountpoint: monitor, 49 | port: 8000, public: 0, 50 | sourcepassword: icecast_source_password, 51 | type: icecast, appendtype: 1} 52 | 53 | twitter: {key: 'your access token key', 54 | mode: 0, secret: 'your acess token secret key', 55 | tags: 'parisson deefuzzzer'} 56 | -------------------------------------------------------------------------------- /deefuzzer/tools/twitt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2006-2009 Guillaume Pellerin 5 | 6 | # 7 | 8 | # This file is part of deefuzzer 9 | 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | # Twitter DeeFuzzer keys 25 | DEEFUZZER_CONSUMER_KEY = 'ozs9cPS2ci6eYQzzMSTb4g' 26 | DEEFUZZER_CONSUMER_SECRET = '1kNEffHgGSXO2gMNTr8HRum5s2ofx3VQnJyfd0es' 27 | 28 | 29 | class Twitter: 30 | def __init__(self, key, secret): 31 | import twitter 32 | 33 | self.consumer_key = DEEFUZZER_CONSUMER_KEY 34 | self.consumer_secret = DEEFUZZER_CONSUMER_SECRET 35 | self.access_token_key = key 36 | self.access_token_secret = secret 37 | self.api = twitter.Api(consumer_key=self.consumer_key, 38 | consumer_secret=self.consumer_secret, 39 | access_token_key=self.access_token_key, 40 | access_token_secret=self.access_token_secret) 41 | 42 | def post(self, message): 43 | try: 44 | self.api.PostUpdate(message) 45 | except: 46 | pass 47 | 48 | -------------------------------------------------------------------------------- /debian/manpage.1.ex: -------------------------------------------------------------------------------- 1 | .\" Hey, EMACS: -*- nroff -*- 2 | .\" First parameter, NAME, should be all caps 3 | .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection 4 | .\" other parameters are allowed: see man(7), man(1) 5 | .TH D-FUZZ SECTION "décembre 9, 2007" 6 | .\" Please adjust this date whenever revising the manpage. 7 | .\" 8 | .\" Some roff macros, for reference: 9 | .\" .nh disable hyphenation 10 | .\" .hy enable hyphenation 11 | .\" .ad l left justify 12 | .\" .ad b justify to both left and right margins 13 | .\" .nf disable filling 14 | .\" .fi enable filling 15 | .\" .br insert line break 16 | .\" .sp insert n+1 empty lines 17 | .\" for manpage-specific macros, see man(7) 18 | .SH NAME 19 | deefuzz \- program to do something 20 | .SH SYNOPSIS 21 | .B deefuzz 22 | .RI [ options ] " files" ... 23 | .br 24 | .B bar 25 | .RI [ options ] " files" ... 26 | .SH DESCRIPTION 27 | This manual page documents briefly the 28 | .B deefuzz 29 | and 30 | .B bar 31 | commands. 32 | .PP 33 | .\" TeX users may be more comfortable with the \fB\fP and 34 | .\" \fI\fP escape sequences to invode bold face and italics, 35 | .\" respectively. 36 | \fBdeefuzz\fP is a program that... 37 | .SH OPTIONS 38 | These programs follow the usual GNU command line syntax, with long 39 | options starting with two dashes (`-'). 40 | A summary of options is included below. 41 | For a complete description, see the Info files. 42 | .TP 43 | .B \-h, \-\-help 44 | Show summary of options. 45 | .TP 46 | .B \-v, \-\-version 47 | Show version of program. 48 | .SH SEE ALSO 49 | .BR bar (1), 50 | .BR baz (1). 51 | .br 52 | The programs are documented fully by 53 | .IR "The Rise and Fall of a Fooish Bar" , 54 | available via the Info system. 55 | .SH AUTHOR 56 | deefuzz was written by . 57 | .PP 58 | This manual page was written by G. Pellerin , 59 | for the Debian project (but may be used by others). 60 | -------------------------------------------------------------------------------- /deefuzzer/tools/streamer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2006-2011 Guillaume Pellerin 5 | 6 | # 7 | 8 | # This file is part of deefuzzer 9 | 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | from threading import Thread 25 | 26 | 27 | class HTTPStreamer(Thread): 28 | protocol = 'http' 29 | host = str 30 | port = str 31 | mount = str 32 | user = str 33 | password = str 34 | public = str 35 | audio_info = dict 36 | name = str 37 | genre = str 38 | decription = str 39 | format = str 40 | url = str 41 | delay = 0 42 | 43 | def __init__(self): 44 | Thread.__init__(self) 45 | import pycurl 46 | 47 | self.curl = pycurl.Curl() 48 | 49 | def set_callback(self, read_callback): 50 | self.read_callback = read_callback 51 | 52 | def delay(self): 53 | return self.delay 54 | 55 | def open(self): 56 | import pycurl 57 | 58 | self.uri = self.protocol + '://' + self.host + ':' + str(self.port) 59 | self.uri += self.mount + '?' + 'password=' + self.password 60 | self.curl.setopt(pycurl.URL, self.uri) 61 | self.curl.setopt(pycurl.UPLOAD, 1) 62 | self.curl.setopt(pycurl.READFUNCTION, self.read_callback) 63 | 64 | def run(self): 65 | self.curl.perform() 66 | 67 | def close(self): 68 | self.curl.close() 69 | -------------------------------------------------------------------------------- /deefuzzer/relay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2006-2011 Guillaume Pellerin 5 | 6 | # 7 | 8 | # This file is part of deefuzzer 9 | 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | from threading import Thread 25 | import queue 26 | import urllib.request, urllib.parse, urllib.error 27 | 28 | 29 | class Relay(Thread): 30 | 31 | def __init__(self, sub_buffer_size, queue_size): 32 | Thread.__init__(self) 33 | self.sub_buffer_size = sub_buffer_size 34 | self.queue_size = queue_size 35 | self.queue = queue.Queue(self.queue_size) 36 | self.stream = None 37 | 38 | def set_url(self, url): 39 | self.url = url 40 | 41 | def open(self): 42 | try: 43 | self.stream = urllib.request.urlopen(self.url) 44 | self.isopen = True 45 | except: 46 | self.isopen = False 47 | 48 | def close(self): 49 | if self.stream: 50 | self.isopen = False 51 | 52 | def run(self): 53 | while True: 54 | if self.isopen: 55 | self.chunk = self.stream.read(self.sub_buffer_size) 56 | self.queue.put_nowait(self.chunk) 57 | # print(self.queue.qsize()) 58 | else: 59 | if self.stream: 60 | self.stream.close() 61 | else: 62 | self.open() 63 | 64 | -------------------------------------------------------------------------------- /example/deefuzzer_telecaster_test.yaml: -------------------------------------------------------------------------------- 1 | deefuzzer: 2 | log: /tmp/telecaster_webm_monitor.log 3 | m3u: /tmp/telecaster_webm_monitor.m3u 4 | station: 5 | control: {mode: 1, port: 16002} 6 | infos: {description: TeleCaster WebM monitor stream, genre: Vocal, name: monitor_0.webm, short_name: monitor_0.webm, 7 | url: 'https://www.parisson.com'} 8 | jingles: {dir: /path/to/jingles, mode: '0', shuffle: '0'} 9 | media: {bitrate: '512', dir: /usr/local/share/telecaster/media/webm, format: webm, ogg_quality: '4', 10 | samplerate: '48000', shuffle: '0', voices: '1'} 11 | record: {dir: /tmp/webm/0, mode: 1} 12 | relay: {author: TeleCaster, mode: '1', url: 'http://127.0.0.1:17000/telecaster_live_0.webm'} 13 | rss: {dir: /var/www/rss, enclosure: '0', media_url: 'http://localhost/rss/'} 14 | server: {host: 127.0.0.1, port: '8000', public: '0', sourcepassword: source2parisson, 15 | type: icecast, mountpoint: monitor_0.webm} 16 | twitter: {key: 76728330-OjKgbHtn4II86Ad7pNUGEzfNAkGTW5Wvw38qUmLE, mode: '0', secret: 4egZs1dSM37XVY8zXa016Yueku2fleXF2bx8k25V4, 17 | tags: bla bla} 18 | station: 19 | control: {mode: 1, port: 16012} 20 | infos: {description: TeleCaster WebM monitor stream, genre: Vocal, name: monitor_2.webm, short_name: monitor_2.webm, 21 | url: 'https://www.parisson.com'} 22 | jingles: {dir: /path/to/jingles, mode: '0', shuffle: '0'} 23 | media: {bitrate: '512', dir: /usr/local/share/telecaster/media/webm, format: webm, ogg_quality: '4', 24 | samplerate: '48000', shuffle: '0', voices: '1'} 25 | record: {dir: /tmp/webm/2, mode: 1} 26 | relay: {author: TeleCaster, mode: '1', url: 'http://127.0.0.1:17000/telecaster_live_2.webm'} 27 | rss: {dir: /var/www/rss, enclosure: '0', media_url: 'http://localhost/rss/'} 28 | server: {host: 127.0.0.1, port: '8000', public: '0', sourcepassword: source2parisson, 29 | type: icecast, mountpoint: monitor_2.webm} 30 | twitter: {key: 76728330-OjKgbHtn4II86Ad7pNUGEzfNAkGTW5Wvw38qUmLE, mode: '0', secret: 4egZs1dSM37XVY8zXa016Yueku2fleXF2bx8k25V4, 31 | tags: bla bla} -------------------------------------------------------------------------------- /deefuzzer/streamer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2006-2011 Guillaume Pellerin 5 | 6 | # 7 | 8 | # This file is part of deefuzzer 9 | 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | from threading import Thread 25 | 26 | 27 | class HTTPStreamer(Thread): 28 | protocol = 'http' 29 | host = str 30 | port = str 31 | mount = str 32 | user = str 33 | password = str 34 | public = str 35 | audio_info = dict 36 | name = str 37 | genre = str 38 | decription = str 39 | format = str 40 | url = str 41 | _delay = 0 42 | 43 | def __init__(self): 44 | Thread.__init__(self) 45 | import pycurl 46 | 47 | self.curl = pycurl.Curl() 48 | 49 | def set_callback(self, read_callback): 50 | self.read_callback = read_callback 51 | 52 | def delay(self): 53 | return self._delay 54 | 55 | def open(self): 56 | import pycurl 57 | 58 | self.uri = self.protocol + '://' + self.host + ':' + str(self.port) 59 | self.uri += self.mount + '?' + 'password=' + self.password 60 | self.curl.setopt(pycurl.URL, self.uri) 61 | self.curl.setopt(pycurl.NOSIGNAL, 1) 62 | self.curl.setopt(pycurl.UPLOAD, 1) 63 | self.curl.setopt(pycurl.READFUNCTION, self.read_callback) 64 | 65 | def run(self): 66 | self.curl.perform() 67 | 68 | def close(self): 69 | self.curl.close() 70 | -------------------------------------------------------------------------------- /scripts/etc/init.d/deefuzzer: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: deefuzzer 4 | # Required-Start: $remote_fs $network 5 | # Required-Stop: $remote_fs 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: Starts the deefuzzer streaming player 9 | ### END INIT INFO 10 | # 11 | # deefuzzer 12 | # 13 | # Written By Dennis Wallace (github@achbed.org) 14 | # Based on icecast2 by Miquel van Smoorenburg 15 | . 16 | # Modified for Debian 17 | # by Ian Murdock . 18 | # 19 | # Further modified by Keegan Quinn 20 | # for use with Icecast 2 21 | # 22 | 23 | PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 24 | DAEMON=/usr/local/bin/deefuzzer 25 | NAME=deefuzzer 26 | DESC=deefuzzer 27 | 28 | test -x $DAEMON || exit 0 29 | 30 | . /lib/lsb/init-functions 31 | 32 | # Defaults 33 | CONFIGFILE="/etc/deefuzzer/deefuzzer.xml" 34 | CONFIGDEFAULTFILE="/etc/default/deefuzzer" 35 | USERID=deefuzzer 36 | GROUPID=deefuzzer 37 | ENABLE="false" 38 | QUIET="--quiet" 39 | PIDFILE=/"var/log/deefuzzer/deefuzzer.pid" 40 | 41 | # Reads config file (will override defaults above) 42 | [ -r "$CONFIGDEFAULTFILE" ] && . $CONFIGDEFAULTFILE 43 | 44 | if [ "$ENABLE" != "true" ]; then 45 | echo "$NAME daemon disabled - read $CONFIGDEFAULTFILE." 46 | exit 0 47 | fi 48 | 49 | set -e 50 | 51 | case "$1" in 52 | start) 53 | echo -n "Starting $DESC: " 54 | start-stop-daemon --start --background -m --oknodo --pidfile ${PIDFILE} 55 | --exec ${DAEMON} -- ${CONFIGFILE} 56 | echo "$NAME." 57 | ;; 58 | stop) 59 | echo -n "Stopping $DESC: " 60 | start-stop-daemon --stop --oknodo --pidfile ${PIDFILE} 61 | rm -f ${PIDFILE} 62 | echo "$NAME." 63 | ;; 64 | reload|force-reload|restart) 65 | echo -n "Restarting $DESC: " 66 | start-stop-daemon --stop --oknodo --pidfile ${PIDFILE} 67 | rm -f ${PIDFILE} 68 | start-stop-daemon --start --background -m --oknodo --pidfile ${PIDFILE} 69 | --exec ${DAEMON} -- ${CONFIGFILE} 70 | echo "$NAME." 71 | ;; 72 | *) 73 | echo "Usage: $0 {start|stop|restart|reload|force-reload}" >&2 74 | exit 1 75 | ;; 76 | esac 77 | 78 | exit 0 79 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | This package was debianized by Guillaume Pellerin on 2 | Sun, 09 Dec 2007 21:50:43 +0100. 3 | 4 | It was downloaded from http://svn.parisson.org/deefuzz 5 | 6 | Upstream Author(s): 7 | 8 | Guillaume Pellerin 9 | 10 | Copyright: 11 | 12 | Copyright (C) 2007-2009 Guillaume Pellerin 13 | 14 | License: 15 | 16 | This software is a computer program whose purpose is to stream audio 17 | and video data through icecast2 servers. 18 | 19 | This software is governed by the CeCILL license under French law and 20 | abiding by the rules of distribution of free software. You can use, 21 | modify and/ or redistribute the software under the terms of the CeCILL 22 | license as circulated by CEA, CNRS and INRIA at the following URL 23 | "http://www.cecill.info". 24 | 25 | As a counterpart to the access to the source code and rights to copy, 26 | modify and redistribute granted by the license, users are provided only 27 | with a limited warranty and the software's author, the holder of the 28 | economic rights, and the successive licensors have only limited 29 | liability. 30 | 31 | In this respect, the user's attention is drawn to the risks associated 32 | with loading, using, modifying and/or developing or reproducing the 33 | software by the user in light of its specific status of free software, 34 | that may mean that it is complicated to manipulate, and that also 35 | therefore means that it is reserved for developers and experienced 36 | professionals having in-depth computer knowledge. Users are therefore 37 | encouraged to load and test the software's suitability as regards their 38 | requirements in conditions enabling the security of their systems and/or 39 | data to be ensured and, more generally, to use and operate it in the 40 | same conditions as regards security. 41 | 42 | The fact that you are presently reading this means that you have had 43 | knowledge of the CeCILL license and that you accept its terms. 44 | 45 | The Debian packaging is (C) 2009, Guillaume Pellerin 46 | and is licensed under the CeCILL v2 licence, see http://www.cecill.info. 47 | 48 | Please also look if there are files or directories which have a 49 | different copyright/license attached and list them here. 50 | -------------------------------------------------------------------------------- /deefuzzer/tools/webm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright Guillaume Pellerin (2006-2009) 5 | 6 | # 7 | 8 | # This software is a computer program whose purpose is to stream audio 9 | # and video data through icecast2 servers. 10 | 11 | # This software is governed by the CeCILL license under French law and 12 | # abiding by the rules of distribution of free software. You can use, 13 | # modify and/ or redistribute the software under the terms of the CeCILL 14 | # license as circulated by CEA, CNRS and INRIA at the following URL 15 | # "http://www.cecill.info". 16 | 17 | # As a counterpart to the access to the source code and rights to copy, 18 | # modify and redistribute granted by the license, users are provided only 19 | # with a limited warranty and the software's author, the holder of the 20 | # economic rights, and the successive licensors have only limited 21 | # liability. 22 | 23 | # In this respect, the user's attention is drawn to the risks associated 24 | # with loading, using, modifying and/or developing or reproducing the 25 | # software by the user in light of its specific status of free software, 26 | # that may mean that it is complicated to manipulate, and that also 27 | # therefore means that it is reserved for developers and experienced 28 | # professionals having in-depth computer knowledge. Users are therefore 29 | # encouraged to load and test the software's suitability as regards their 30 | # requirements in conditions enabling the security of their systems and/or 31 | # data to be ensured and, more generally, to use and operate it in the 32 | # same conditions as regards security. 33 | 34 | # The fact that you are presently reading this means that you have had 35 | # knowledge of the CeCILL license and that you accept its terms. 36 | 37 | # Author: Guillaume Pellerin 38 | 39 | import os 40 | import string 41 | import datetime 42 | from .utils import * 43 | 44 | 45 | class WebM(MediaBase): 46 | """An WebM file object""" 47 | 48 | def __init__(self, media): 49 | MediaBase.__init__(self) 50 | 51 | self.description = "WebM" 52 | self.mime_type = 'video/webm' 53 | self.extension = 'webm' 54 | self.format = 'WebM' 55 | 56 | self.media = media 57 | self.source = self.media 58 | self.file_name, self.file_title, self.file_ext = get_file_info(media) 59 | -------------------------------------------------------------------------------- /example/deefuzzer_webm.json: -------------------------------------------------------------------------------- 1 | { 2 | "deefuzzer": { 3 | "log": "~/tmp/station.log", 4 | "m3u": "~/tmp/station.m3u", 5 | "stationdefaults": { 6 | "control": { 7 | "mode": 0, 8 | "port": 16001 9 | }, 10 | "jingles": { 11 | "dir": "/path/to/jingles", 12 | "mode": 0, 13 | "shuffle": 1 14 | } 15 | }, 16 | "station": { 17 | "control": { 18 | "mode": 0, 19 | "port": 16001 20 | }, 21 | "infos": { 22 | "description": "My personal best funky playlist ever !", 23 | "genre": "Various Funk Groove", 24 | "name": "My best funky station", 25 | "short_name": "My_station", 26 | "url": "http://parisson.com" 27 | }, 28 | "jingles": { 29 | "dir": "/path/to/jingles", 30 | "mode": 0, 31 | "shuffle": 1 32 | }, 33 | "media": { 34 | "bitrate": 96, 35 | "source": "/home/guyom/Videos/test/webm/", 36 | "format": "webm", 37 | "ogg_quality": 4, 38 | "samplerate": 48000, 39 | "shuffle": 0, 40 | "voices": 2 41 | }, 42 | "record": { 43 | "dir": "/path/to/archives", 44 | "mode": 0 45 | }, 46 | "relay": { 47 | "author": "Unknown", 48 | "mode": 0, 49 | "url": "http://127.0.0.1:8000/telecaster_live.mp3" 50 | }, 51 | "feeds": { 52 | "mode": 1, 53 | "rss": 1, 54 | "json": 0, 55 | "playlist": 0, 56 | "dir": "~/tmp/rss", 57 | "enclosure": 1, 58 | "media_url": "http://localhost/media/" 59 | }, 60 | "server": { 61 | "host": "localhost", 62 | "mountpoint": "monitor", 63 | "port": 8000, 64 | "public": 0, 65 | "sourcepassword": "hackme", 66 | "type": "icecast", 67 | "appendtype": 1 68 | }, 69 | "twitter": { 70 | "key": "you access token key", 71 | "mode": 0, 72 | "secret": "your access token secret key", 73 | "tags": "parisson deefuzzer" 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /example/deefuzzer_webm_relay.json: -------------------------------------------------------------------------------- 1 | { 2 | "deefuzzer": { 3 | "log": "/tmp/station.log", 4 | "m3u": "/tmp/station.m3u", 5 | "stationdefaults": { 6 | "control": { 7 | "mode": 0, 8 | "port": 16001 9 | }, 10 | "jingles": { 11 | "dir": "/path/to/jingles", 12 | "mode": 0, 13 | "shuffle": 1 14 | } 15 | }, 16 | "station": { 17 | "control": { 18 | "mode": 0, 19 | "port": 16001 20 | }, 21 | "infos": { 22 | "description": "My personal best funky playlist ever !", 23 | "genre": "Various Funk Groove", 24 | "name": "My best funky station", 25 | "short_name": "My_station", 26 | "url": "http://parisson.com" 27 | }, 28 | "jingles": { 29 | "dir": "/path/to/jingles", 30 | "mode": 0, 31 | "shuffle": 1 32 | }, 33 | "media": { 34 | "bitrate": 96, 35 | "source": "/home/guyom/Vidéos/test/webm/", 36 | "format": "webm", 37 | "ogg_quality": 4, 38 | "samplerate": 48000, 39 | "shuffle": 0, 40 | "voices": 2 41 | }, 42 | "record": { 43 | "dir": "/path/to/archives", 44 | "mode": 0 45 | }, 46 | "relay": { 47 | "author": "Unknown", 48 | "mode": 1, 49 | "url": "http://localhost:17032/monitor.webm" 50 | }, 51 | "feeds": { 52 | "mode": 0, 53 | "rss": 1, 54 | "json": 0, 55 | "playlist": 0, 56 | "dir": "~/tmp/rss", 57 | "enclosure": 1, 58 | "media_url": "http://localhost/media/" 59 | }, 60 | "server": { 61 | "host": "localhost", 62 | "mountpoint": "monitor", 63 | "port": 8000, 64 | "public": 0, 65 | "sourcepassword": "hackme", 66 | "type": "icecast", 67 | "appendtype": 1 68 | }, 69 | "twitter": { 70 | "key": "you access token key", 71 | "mode": 0, 72 | "secret": "your access token secret key", 73 | "tags": "parisson deefuzzer" 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /example/deefuzzer_webm_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "deefuzzer": { 3 | "log": "/tmp/station.log", 4 | "m3u": "/tmp/station.m3u", 5 | "stationdefaults": { 6 | "control": { 7 | "mode": 0, 8 | "port": 16001 9 | }, 10 | "jingles": { 11 | "dir": "/path/to/jingles", 12 | "mode": 0, 13 | "shuffle": 1 14 | } 15 | }, 16 | "station": { 17 | "control": { 18 | "mode": 0, 19 | "port": 16001 20 | }, 21 | "infos": { 22 | "description": "My personal best funky playlist ever !", 23 | "genre": "Various Funk Groove", 24 | "name": "My best funky station", 25 | "short_name": "My_station", 26 | "url": "http://parisson.com" 27 | }, 28 | "jingles": { 29 | "dir": "/path/to/jingles", 30 | "mode": 0, 31 | "shuffle": 1 32 | }, 33 | "media": { 34 | "bitrate": 96, 35 | "source": "/home/guyom/Vidéos/test/webm/", 36 | "format": "webm", 37 | "ogg_quality": 4, 38 | "samplerate": 48000, 39 | "shuffle": 0, 40 | "voices": 2 41 | }, 42 | "record": { 43 | "dir": "/path/to/archives", 44 | "mode": 0 45 | }, 46 | "relay": { 47 | "author": "Unknown", 48 | "mode": 0, 49 | "url": "http://127.0.0.1:8000/telecaster_live.mp3" 50 | }, 51 | "feeds": { 52 | "mode": 1, 53 | "rss": 1, 54 | "json": 0, 55 | "playlist": 0, 56 | "dir": "~/tmp/rss", 57 | "enclosure": 1, 58 | "media_url": "http://localhost/media/" 59 | }, 60 | "server": { 61 | "host": "localhost", 62 | "mountpoint": "monitor", 63 | "port": 8000, 64 | "public": 0, 65 | "sourcepassword": "hackme", 66 | "type": "icecast", 67 | "appendtype": 1 68 | }, 69 | "twitter": { 70 | "key": "you access token key", 71 | "mode": 0, 72 | "secret": "your access token secret key", 73 | "tags": "parisson deefuzzer" 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /example/deefuzzer.xml: -------------------------------------------------------------------------------- 1 | 2 | /path/to/station.log 3 | /path/to/station.m3u 4 | 5 | 6 | 0 7 | 16001 8 | 9 | 10 | /path/to/jingles 11 | 0 12 | 1 13 | 14 | 15 | 16 | 17 | 0 18 | 16001 19 | 20 | 21 | My personal best funky playlist ever ! 22 | Various Funk Groove 23 | My best funky station 24 | My_station 25 | http://parisson.com 26 | 27 | 28 | /path/to/jingles 29 | 0 30 | 1 31 | 32 | 33 | 96 34 | /path/to/mp3/or/m3u 35 | mp3 36 | 4 37 | 48000 38 | 0 39 | 2 40 | 41 | 42 | /path/to/archives 43 | 0 44 | 45 | 46 | Unknown 47 | 0 48 | http://127.0.0.1:8000/telecaster_live.mp3 49 | 50 | 51 | 1 52 | 1 53 | 0 54 | 1 55 | /path/to/rss/ 56 | 1 57 | http://localhost/media/ 58 | 59 | 60 | 127.0.0.1 61 | monitor 62 | 8000 63 | 0 64 | icecast_source_password 65 | icecast 66 | 1 67 | 68 | 69 | your access token key 70 | 0 71 | your access token secret key 72 | parisson deefuzzer 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /example/deefuzzer_test.xml: -------------------------------------------------------------------------------- 1 | 2 | /tmp/station.log 3 | /tmp/station.m3u 4 | 5 | 6 | 0 7 | 16001 8 | 9 | 10 | /path/to/jingles 11 | 0 12 | 1 13 | 14 | 15 | 16 | 17 | 0 18 | 16001 19 | 20 | 21 | My personal best funky playlist ever ! 22 | Various Funk Groove 23 | My best funky station 24 | My_station 25 | http://parisson.com 26 | 27 | 28 | /path/to/jingles 29 | 0 30 | 1 31 | 32 | 33 | 96 34 | /home/guyom/Musique/MP3/NovaTunes 35 | mp3 36 | 4 37 | 48000 38 | 0 39 | 2 40 | 41 | 42 | /path/to/archives 43 | 0 44 | 45 | 46 | Unknown 47 | 0 48 | http://127.0.0.1:8000/telecaster_live.mp3 49 | 50 | 51 | 1 52 | 1 53 | 1 54 | 1 55 | /tmp/feeds 56 | 1 57 | http://localhost/media/ 58 | 59 | 60 | stream7.parisson.com 61 | monitor 62 | 8000 63 | 0 64 | source2parisson 65 | icecast 66 | 1 67 | 68 | 69 | your access token key 70 | 0 71 | your access token secret key 72 | parisson deefuzzer 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /example/deefuzzer_multiple.yaml: -------------------------------------------------------------------------------- 1 | deefuzzer: 2 | log: "/tmp/station.log" 3 | m3u: "/tmp/station.m3u" 4 | stationdefaults: 5 | control: 6 | mode: 0 7 | port: 16001 8 | jingles: 9 | dir: "/path/to/jingles" 10 | mode: 0 11 | shuffle: 1 12 | station: 13 | - 14 | control: 15 | mode: 0 16 | port: 16001 17 | infos: 18 | description: "My personal best funky playlist ever !" 19 | genre: "Various Funk Groove" 20 | name: "My best funky station" 21 | short_name: My_station 22 | url: "http://parisson.com" 23 | jingles: 24 | dir: "/path/to/jingles" 25 | mode: 0 26 | shuffle: 1 27 | media: 28 | bitrate: 96 29 | source: "/home/yomguy/Musique/MP3" 30 | format: mp3 31 | ogg_quality: 4 32 | samplerate: 48000 33 | shuffle: 0 34 | voices: 2 35 | record: 36 | dir: "/path/to/archives" 37 | mode: 0 38 | relay: 39 | author: Unknown 40 | mode: 0 41 | url: "http://127.0.0.1:8000/telecaster_live.mp3" 42 | feeds: 43 | mode: 0 44 | rss: 1 45 | json: 0 46 | playlist: 1 47 | dir: "/path/to/rss/" 48 | enclosure: 1 49 | media_url: "http://localhost/media/" 50 | server: 51 | host: "127.0.0.1" 52 | mountpoint: monitor0 53 | port: 8000 54 | public: 0 55 | sourcepassword: hackme 56 | type: icecast 57 | appendtype: 1 58 | twitter: 59 | key: "your access token key" 60 | mode: 0 61 | secret: "your access token secret key" 62 | tags: "parisson deefuzzer" 63 | - 64 | control: 65 | mode: 0 66 | port: 16001 67 | infos: 68 | description: "My personal best funky playlist ever !" 69 | genre: "Various Funk Groove" 70 | name: "My best funky station" 71 | short_name: My_station 72 | url: "http://parisson.com" 73 | jingles: 74 | dir: "/path/to/jingles" 75 | mode: 0 76 | shuffle: 1 77 | media: 78 | bitrate: 96 79 | source: "/home/yomguy/Musique/MP3" 80 | format: mp3 81 | ogg_quality: 4 82 | samplerate: 48000 83 | shuffle: 0 84 | voices: 2 85 | record: 86 | dir: "/path/to/archives" 87 | mode: 0 88 | relay: 89 | author: Unknown 90 | mode: 0 91 | url: "http://127.0.0.1:8000/telecaster_live.mp3" 92 | feeds: 93 | mode: 0 94 | rss: 1 95 | json: 0 96 | playlist: 1 97 | dir: "/path/to/rss/" 98 | enclosure: 1 99 | media_url: "http://localhost/media/" 100 | server: 101 | host: "127.0.0.1" 102 | mountpoint: monitor1 103 | port: 8000 104 | public: 0 105 | sourcepassword: hackme 106 | type: icecast 107 | appendtype: 1 108 | twitter: 109 | key: "your access token key" 110 | mode: 0 111 | secret: "your access token secret key" 112 | tags: "parisson deefuzzer" 113 | -------------------------------------------------------------------------------- /deefuzzer/tools/get_access_token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright 2007 The Python-Twitter Developers 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | import sys 19 | 20 | # parse_qsl moved to urlparse module in v2.6 21 | try: 22 | from urllib.parse import parse_qsl 23 | except: 24 | from cgi import parse_qsl 25 | 26 | import oauth2 as oauth 27 | 28 | REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' 29 | ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' 30 | AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' 31 | SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' 32 | 33 | consumer_key = 'ozs9cPS2ci6eYQzzMSTb4g' 34 | consumer_secret = '1kNEffHgGSXO2gMNTr8HRum5s2ofx3VQnJyfd0es' 35 | 36 | if consumer_key is None or consumer_secret is None: 37 | print('You need to edit this script and provide values for the') 38 | print('consumer_key and also consumer_secret.') 39 | print('') 40 | print('The values you need come from Twitter - you need to register') 41 | print('as a developer your "application". This is needed only until') 42 | print('Twitter finishes the idea they have of a way to allow open-source') 43 | print('based libraries to have a token that can be used to generate a') 44 | print('one-time use key that will allow the library to make the request') 45 | print('on your behalf.') 46 | print('') 47 | sys.exit(1) 48 | 49 | signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() 50 | oauth_consumer = oauth.Consumer(key=consumer_key, secret=consumer_secret) 51 | oauth_client = oauth.Client(oauth_consumer) 52 | 53 | print('Requesting temp token from Twitter') 54 | 55 | resp, content = oauth_client.request(REQUEST_TOKEN_URL, 'GET') 56 | 57 | if resp['status'] != '200': 58 | print('Invalid respond from Twitter requesting temp token: %s' % resp['status']) 59 | else: 60 | request_token = dict(parse_qsl(content)) 61 | 62 | print('') 63 | print('Please visit this Twitter page and retrieve the pincode to be used') 64 | print('in the next step to obtaining an Authentication Token:') 65 | print('') 66 | print('%s?oauth_token=%s' % (AUTHORIZATION_URL, request_token['oauth_token'])) 67 | print('') 68 | 69 | pincode = input('Pincode? ') 70 | 71 | token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) 72 | token.set_verifier(pincode) 73 | 74 | print('') 75 | print('Generating and signing request for an access token') 76 | print('') 77 | 78 | oauth_client = oauth.Client(oauth_consumer, token) 79 | resp, content = oauth_client.request(ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % pincode) 80 | access_token = dict(parse_qsl(content)) 81 | 82 | if resp['status'] != '200': 83 | print('The request for a Token did not succeed: %s' % resp['status']) 84 | print(access_token) 85 | else: 86 | print('Your Twitter Access Token key: %s' % access_token['oauth_token']) 87 | print(' Access Token secret: %s' % access_token['oauth_token_secret']) 88 | print('') 89 | 90 | -------------------------------------------------------------------------------- /example/deefuzzer_webm_relay_multiple.yaml: -------------------------------------------------------------------------------- 1 | deefuzzer: 2 | log: "/tmp/station.log" 3 | m3u: "/tmp/station.m3u" 4 | stationdefaults: 5 | control: 6 | mode: 0 7 | port: 16001 8 | jingles: 9 | dir: "/path/to/jingles" 10 | mode: 0 11 | shuffle: 1 12 | station: 13 | - 14 | control: 15 | mode: 0 16 | port: 16001 17 | infos: 18 | name: "monitor_1" 19 | short_name: "monitor_1" 20 | description: "bla bla" 21 | genre: "testing" 22 | url: "http://parisson.com" 23 | jingles: 24 | dir: "/path/to/jingles" 25 | mode: 0 26 | shuffle: 1 27 | media: 28 | bitrate: 96 29 | source: "/home/guyom/Vidéos/test/webm" 30 | format: webm 31 | ogg_quality: 4 32 | samplerate: 48000 33 | shuffle: 0 34 | voices: 2 35 | record: 36 | dir: "/tmp/deefuzzer/1" 37 | mode: 1 38 | relay: 39 | author: Unknown 40 | mode: 1 41 | url: "http://localhost:17032/monitor.webm" 42 | feeds: 43 | mode: 0 44 | rss: 1 45 | json: 0 46 | playlist: 1 47 | dir: "/path/to/rss/" 48 | enclosure: 1 49 | media_url: "http://localhost/media/" 50 | server: 51 | host: "127.0.0.1" 52 | mountpoint: monitor_1 53 | port: 8000 54 | public: 0 55 | sourcepassword: hackme 56 | type: icecast 57 | appendtype: 1 58 | twitter: 59 | key: "your access token key" 60 | mode: 0 61 | secret: "your access token secret key" 62 | tags: "parisson deefuzzer" 63 | - 64 | control: 65 | mode: 0 66 | port: 16002 67 | infos: 68 | name: "monitor_2" 69 | short_name: "monitor_2" 70 | description: "bla bla" 71 | genre: "testing" 72 | url: "http://parisson.com" 73 | jingles: 74 | dir: "/path/to/jingles" 75 | mode: 0 76 | shuffle: 1 77 | media: 78 | bitrate: 96 79 | source: "/home/guyom/Vidéos/test/webm" 80 | format: webm 81 | ogg_quality: 4 82 | samplerate: 48000 83 | shuffle: 0 84 | voices: 2 85 | record: 86 | dir: "/tmp/deefuzzer/2" 87 | mode: 1 88 | relay: 89 | author: Unknown 90 | mode: 1 91 | url: "http://localhost:17033/monitor.webm" 92 | feeds: 93 | mode: 0 94 | rss: 1 95 | json: 0 96 | playlist: 1 97 | dir: "/path/to/rss/" 98 | enclosure: 1 99 | media_url: "http://localhost/media/" 100 | server: 101 | host: "127.0.0.1" 102 | mountpoint: monitor_2 103 | port: 8000 104 | public: 0 105 | sourcepassword: hackme 106 | type: icecast 107 | appendtype: 1 108 | twitter: 109 | key: "your access token key" 110 | mode: 0 111 | secret: "your access token secret key" 112 | tags: "parisson deefuzzer" 113 | - 114 | control: 115 | mode: 0 116 | port: 16003 117 | infos: 118 | name: "monitor_3" 119 | short_name: "monitor_3" 120 | description: "bla bla" 121 | genre: "testing" 122 | url: "http://parisson.com" 123 | jingles: 124 | dir: "/path/to/jingles" 125 | mode: 0 126 | shuffle: 1 127 | media: 128 | bitrate: 96 129 | source: "/home/guyom/Vidéos/test/webm" 130 | format: webm 131 | ogg_quality: 4 132 | samplerate: 48000 133 | shuffle: 0 134 | voices: 2 135 | record: 136 | dir: "/tmp/deefuzzer/3" 137 | mode: 1 138 | relay: 139 | author: Unknown 140 | mode: 1 141 | url: "http://localhost:17041/monitor.webm" 142 | feeds: 143 | mode: 0 144 | rss: 1 145 | json: 0 146 | playlist: 1 147 | dir: "/path/to/rss/" 148 | enclosure: 1 149 | media_url: "http://localhost/media/" 150 | server: 151 | host: "127.0.0.1" 152 | mountpoint: monitor_3 153 | port: 8000 154 | public: 0 155 | sourcepassword: hackme 156 | type: icecast 157 | appendtype: 1 158 | twitter: 159 | key: "your access token key" 160 | mode: 0 161 | secret: "your access token secret key" 162 | tags: "parisson deefuzzer" 163 | -------------------------------------------------------------------------------- /example/deefuzzer.json: -------------------------------------------------------------------------------- 1 | { 2 | "deefuzzer": { 3 | "log": "/path/to/station.log", 4 | "m3u": "/path/to/station.m3u", 5 | "stationdefaults": { 6 | "control": { 7 | "mode": 0, 8 | "port": 16001 9 | }, 10 | "jingles": { 11 | "dir": "/path/to/jingles", 12 | "mode": 0, 13 | "shuffle": 1 14 | } 15 | }, 16 | "station": { 17 | "control": { 18 | "mode": 0, 19 | "port": 16001 20 | }, 21 | "jingles": { 22 | "dir": "/path/to/jingles", 23 | "mode": 0, 24 | "shuffle": 1 25 | } 26 | }, 27 | "station": { 28 | "control": { 29 | "mode": 0, 30 | "port": 16001 31 | }, 32 | "infos": { 33 | "description": "My personal best funky playlist ever !", 34 | "genre": "Various Funk Groove", 35 | "name": "My best funky station", 36 | "short_name": "My_station", 37 | "url": "http://parisson.com" 38 | }, 39 | "jingles": { 40 | "dir": "/path/to/jingles", 41 | "mode": 0, 42 | "shuffle": 1 43 | }, 44 | "media": { 45 | "bitrate": 96, 46 | "source": "/path/to/mp3/or/m3u", 47 | "format": "mp3", 48 | "ogg_quality": 4, 49 | "samplerate": 48000, 50 | "shuffle": 0, 51 | "voices": 2 52 | }, 53 | "record": { 54 | "dir": "/path/to/archives", 55 | "mode": 0 56 | }, 57 | "relay": { 58 | "author": "Unknown", 59 | "mode": 0, 60 | "url": "http://127.0.0.1:8000/telecaster_live.mp3" 61 | }, 62 | "feeds": { 63 | "mode": 1, 64 | "rss": 1, 65 | "json": 0, 66 | "playlist": 0, 67 | "dir": "/path/to/rss", 68 | "enclosure": 1, 69 | "media_url": "http://localhost/media/" 70 | }, 71 | "server": { 72 | "host": "127.0.0.1", 73 | "mountpoint": "monitor", 74 | "port": 8000, 75 | "public": 0, 76 | "sourcepassword": "icecast_source_password", 77 | "type": "icecast", 78 | "appendtype": 1 79 | }, 80 | "twitter": { 81 | "key": "you access token key", 82 | "mode": 0, 83 | "secret": "your access token secret key", 84 | "tags": "parisson deefuzzer" 85 | } 86 | }, 87 | "station": { 88 | "control": { 89 | "mode": 0, 90 | "port": 16001 91 | }, 92 | "infos": { 93 | "description": "MySQL funky playlist", 94 | "genre": "Various Funk Groove", 95 | "name": "My best funky station managed with MySQL", 96 | "short_name": "MySQL_station", 97 | "url": "http://parisson.com" 98 | }, 99 | "media": { 100 | "bitrate": 96, 101 | "format": "mp3", 102 | "ogg_quality": 4, 103 | "samplerate": 48000, 104 | "shuffle": 0, 105 | "voices": 2, 106 | "mysql": { 107 | "host": "127.0.0.1", 108 | "port": 3306, 109 | "user": "username", 110 | "password": "password", 111 | "database": "database_name", 112 | "table": "playlist_table", 113 | "field": "absolute_path", 114 | "request_table": "request_table", 115 | "request_field": "absolute_path", 116 | "request_played": "played_bool_field" 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /deefuzzer/player.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2006-2011 Guillaume Pellerin 5 | 6 | # 7 | 8 | # This file is part of deefuzzer 9 | 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | from .relay import * 25 | import time 26 | 27 | 28 | class Player: 29 | """A file streaming iterator""" 30 | 31 | def __init__(self, stream_type='icecast'): 32 | if stream_type == 'icecast': 33 | self.main_buffer_size = 0x100000 34 | self.relay_queue_size = 0x100000 35 | self.sub_buffer_size = 0x10000 36 | elif stream_type == 'stream-m': 37 | self.main_buffer_size = 0x100000 38 | self.relay_queue_size = 0x100000 39 | self.sub_buffer_size = 0x10000 40 | 41 | def set_media(self, media): 42 | self.media = media 43 | 44 | def start_relay(self, url): 45 | self.url = url 46 | self.relay = Relay(self.sub_buffer_size, self.relay_queue_size) 47 | self.relay.set_url(self.url) 48 | self.relay.open() 49 | self.relay.start() 50 | 51 | def stop_relay(self): 52 | self.relay.close() 53 | 54 | def file_read_fast(self): 55 | """Read media and stream data through a generator.""" 56 | m = open(self.media, 'rb') 57 | while True: 58 | __main_chunk = m.read(self.sub_buffer_size) 59 | if not __main_chunk: 60 | break 61 | yield __main_chunk 62 | m.close() 63 | 64 | def file_read_slow(self): 65 | """Read a bigger part of the media and stream the little parts 66 | of the data through a generator""" 67 | m = open(self.media, 'rb') 68 | while True: 69 | self.main_chunk = m.read(self.main_buffer_size) 70 | if not self.main_chunk: 71 | break 72 | i = 0 73 | while True: 74 | start = i * self.sub_buffer_size 75 | end = self.sub_buffer_size + (i * self.sub_buffer_size) 76 | self.sub_chunk = self.main_chunk[start:end] 77 | if not self.sub_chunk: 78 | break 79 | yield self.sub_chunk 80 | i += 1 81 | self.main_chunk = 0 82 | self.sub_chunk = 0 83 | m.close() 84 | 85 | def relay_read(self): 86 | """Read a distant media through its URL""" 87 | while True: 88 | self.sub_chunk = self.relay.queue.get(self.sub_buffer_size) 89 | if not self.sub_chunk: 90 | break 91 | yield self.sub_chunk 92 | self.relay.queue.task_done() 93 | self.sub_chunk = 0 94 | 95 | 96 | class FileReader: 97 | 98 | def __init__(self, fp): 99 | self.fp = open(fp, 'r') 100 | 101 | def read_callback(self, size): 102 | return self.fp.read(size) 103 | 104 | 105 | class URLReader: 106 | 107 | def __init__(self, relay): 108 | self.__relayparam = relay 109 | self.relay = urllib.request.urlopen(self.__relayparam) 110 | self.rec_mode = 0 111 | 112 | def set_recorder(self, recorder, mode=1): 113 | self.rec_mode = mode 114 | self.recorder = recorder 115 | 116 | def read_callback(self, size): 117 | chunk = None 118 | 119 | try: 120 | chunk = self.relay.read(size) 121 | except: 122 | while True: 123 | try: 124 | self.relay = urllib.request.urlopen(self.__relayparam) 125 | chunk = self.relay.read(size) 126 | break 127 | except: 128 | time.sleep(0.5) 129 | continue 130 | 131 | if self.rec_mode == 1 and chunk: 132 | self.recorder.write(chunk) 133 | return chunk 134 | -------------------------------------------------------------------------------- /deefuzzer/tools/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2007-2009 Guillaume Pellerin 5 | # All rights reserved. 6 | # 7 | # This software is licensed as described in the file COPYING, which 8 | # you should have received as part of this distribution. The terms 9 | # are also available at http://svn.parisson.org/deefuzz/wiki/DefuzzLicense. 10 | # 11 | # Author: Guillaume Pellerin 12 | 13 | import os 14 | import re 15 | import string 16 | import mimetypes 17 | from itertools import chain 18 | from xml.etree import cElementTree as ElementTree 19 | from deefuzzer.tools import * 20 | 21 | 22 | mimetypes.add_type('application/x-yaml', '.yaml') 23 | 24 | 25 | def clean_word(word): 26 | """ Return the word without excessive blank spaces, underscores and 27 | characters causing problem to exporters""" 28 | word = re.sub(r"^[^\w]+", "", word) # trim the beginning 29 | word = re.sub(r"[^\w]+$", "", word) # trim the end 30 | word = re.sub(r"_+", "_", word) # squeeze continuous _ to one _ 31 | word = re.sub(r"^[^\w]+", "", word) # trim the beginning _ 32 | # word = string.replace(word,' ','_') 33 | # word = string.capitalize(word) 34 | dict = '&[];"*:,' 35 | for letter in dict: 36 | word = string.replace(word, letter, '_') 37 | return word 38 | 39 | 40 | def get_file_info(media): 41 | file_name = media.split(os.sep)[-1] 42 | file_title = file_name.split('.')[:-1] 43 | file_title = '.'.join(file_title) 44 | file_ext = file_name.split('.')[-1] 45 | return file_name, file_title, file_ext 46 | 47 | 48 | def is_absolute_path(path): 49 | return os.sep == path[0] 50 | 51 | 52 | def merge_defaults(setting, default): 53 | combined = {} 54 | for key in set(chain(setting, default)): 55 | if key in setting: 56 | if key in default: 57 | if isinstance(setting[key], dict) and isinstance(default[key], dict): 58 | combined[key] = merge_defaults(setting[key], default[key]) 59 | else: 60 | combined[key] = setting[key] 61 | else: 62 | combined[key] = setting[key] 63 | else: 64 | combined[key] = default[key] 65 | return combined 66 | 67 | 68 | def replace_all(option, repl): 69 | if isinstance(option, list): 70 | r = [] 71 | for i in option: 72 | r.append(replace_all(i, repl)) 73 | return r 74 | elif isinstance(option, dict): 75 | r = {} 76 | for key in list(option.keys()): 77 | r[key] = replace_all(option[key], repl) 78 | return r 79 | elif isinstance(option, str): 80 | r = option 81 | for key in list(repl.keys()): 82 | r = r.replace('[' + key + ']', repl[key]) 83 | return r 84 | return option 85 | 86 | 87 | def get_conf_dict(path): 88 | filename, ext = os.path.splitext(path) 89 | 90 | # Do the type check first, so we don't load huge files that won't be used 91 | if 'xml' in ext: 92 | confile = open(path, 'r') 93 | data = confile.read() 94 | confile.close() 95 | return xmltodict(data, 'utf-8') 96 | 97 | elif 'yaml' in ext or 'yml' in ext: 98 | import yaml 99 | confile = open(path, 'r') 100 | conf = yaml.safe_load(confile) 101 | return conf 102 | 103 | elif 'json' in ext: 104 | import json 105 | confile = open(path, 'r') 106 | data = confile.read() 107 | confile.close() 108 | return json.loads(data) 109 | 110 | return False 111 | 112 | 113 | def write_conf(conf_dict, path): 114 | filename, ext = os.path.splitext(path) 115 | f = open(path, 'w') 116 | 117 | if 'xml' in ext: 118 | xml_data = dicttoxml(conf_dict) 119 | f.write(xml_data) 120 | 121 | elif 'yaml' in ext or 'yml' in ext: 122 | import yaml 123 | yaml.dump(conf_dict, f) 124 | 125 | elif 'json' in ext: 126 | import json 127 | json.dump(conf_dict, f) 128 | 129 | f.close() 130 | 131 | 132 | def folder_contains_music(folder): 133 | files = os.listdir(folder) 134 | for file in files: 135 | filepath = os.path.join(folder, file) 136 | if os.path.isfile(filepath): 137 | mime_type = mimetypes.guess_type(filepath)[0] 138 | if 'audio/mpeg' in mime_type or 'audio/ogg' in mime_type: 139 | return True 140 | return False 141 | -------------------------------------------------------------------------------- /debian/manpage.xml.ex: -------------------------------------------------------------------------------- 1 | 2 | .
will be generated. You may view the 11 | manual page with: nroff -man .
| less'. A 12 | typical entry in a Makefile or Makefile.am is: 13 | 14 | DB2MAN=/usr/share/sgml/docbook/stylesheet/xsl/nwalsh/\ 15 | manpages/docbook.xsl 16 | XP=xsltproc -''-nonet 17 | 18 | manpage.1: manpage.dbk 19 | $(XP) $(DB2MAN) $< 20 | 21 | The xsltproc binary is found in the xsltproc package. The 22 | XSL files are in docbook-xsl. Please remember that if you 23 | create the nroff version in one of the debian/rules file 24 | targets (such as build), you will need to include xsltproc 25 | and docbook-xsl in your Build-Depends control field. 26 | 27 | --> 28 | 29 | 30 | FIRSTNAME"> 31 | SURNAME"> 32 | 33 | décembre 9, 2007"> 34 | 36 | SECTION"> 37 | yomguy@altern.org"> 38 | 39 | D-FUZZ"> 40 | 41 | 42 | Debian"> 43 | GNU"> 44 | GPL"> 45 | ]> 46 | 47 | 48 | 49 |
50 | &dhemail; 51 |
52 | 53 | 2007 54 | &dhusername; 55 | 56 | &dhdate; 57 |
58 | 59 | &dhucpackage; 60 | 61 | &dhsection; 62 | 63 | 64 | &dhpackage; 65 | 66 | program to do something 67 | 68 | 69 | 70 | &dhpackage; 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | DESCRIPTION 79 | 80 | This manual page documents briefly the 81 | &dhpackage; and bar 82 | commands. 83 | 84 | This manual page was written for the &debian; distribution 85 | because the original program does not have a manual page. 86 | Instead, it has documentation in the &gnu; 87 | Info format; see below. 88 | 89 | &dhpackage; is a program that... 90 | 91 | 92 | 93 | OPTIONS 94 | 95 | These programs follow the usual &gnu; command line syntax, 96 | with long options starting with two dashes (`-'). A summary of 97 | options is included below. For a complete description, see the 98 | Info files. 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | Show summary of options. 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Show version of program. 115 | 116 | 117 | 118 | 119 | 120 | SEE ALSO 121 | 122 | bar (1), baz (1). 123 | 124 | The programs are documented fully by The Rise and 125 | Fall of a Fooish Bar available via the 126 | Info system. 127 | 128 | 129 | AUTHOR 130 | 131 | This manual page was written by &dhusername; &dhemail; for 132 | the &debian; system (but may be used by others). Permission is 133 | granted to copy, distribute and/or modify this document under 134 | the terms of the &gnu; General Public License, Version 2 any 135 | later version published by the Free Software Foundation. 136 | 137 | 138 | On Debian systems, the complete text of the GNU General Public 139 | License can be found in /usr/share/common-licenses/GPL. 140 | 141 | 142 | 143 |
144 | 145 | -------------------------------------------------------------------------------- /deefuzzer/tools/mp3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright Guillaume Pellerin (2006-2009) 5 | 6 | # 7 | 8 | # This software is a computer program whose purpose is to stream audio 9 | # and video data through icecast2 servers. 10 | 11 | # This software is governed by the CeCILL license under French law and 12 | # abiding by the rules of distribution of free software. You can use, 13 | # modify and/ or redistribute the software under the terms of the CeCILL 14 | # license as circulated by CEA, CNRS and INRIA at the following URL 15 | # "http://www.cecill.info". 16 | 17 | # As a counterpart to the access to the source code and rights to copy, 18 | # modify and redistribute granted by the license, users are provided only 19 | # with a limited warranty and the software's author, the holder of the 20 | # economic rights, and the successive licensors have only limited 21 | # liability. 22 | 23 | # In this respect, the user's attention is drawn to the risks associated 24 | # with loading, using, modifying and/or developing or reproducing the 25 | # software by the user in light of its specific status of free software, 26 | # that may mean that it is complicated to manipulate, and that also 27 | # therefore means that it is reserved for developers and experienced 28 | # professionals having in-depth computer knowledge. Users are therefore 29 | # encouraged to load and test the software's suitability as regards their 30 | # requirements in conditions enabling the security of their systems and/or 31 | # data to be ensured and, more generally, to use and operate it in the 32 | # same conditions as regards security. 33 | 34 | # The fact that you are presently reading this means that you have had 35 | # knowledge of the CeCILL license and that you accept its terms. 36 | 37 | # Author: Guillaume Pellerin 38 | 39 | import os 40 | import string 41 | import datetime 42 | from mutagen.easyid3 import EasyID3 43 | from mutagen.mp3 import MP3, MPEGInfo 44 | from mutagen import id3 45 | from .utils import * 46 | 47 | EasyID3.valid_keys["comment"] = "COMM::'XXX'" 48 | EasyID3.valid_keys["copyright"] = "TCOP::'XXX'" 49 | EasyID3.valid_keys["country"] = "TXXX:COUNTRY:'XXX'" 50 | EasyID3.RegisterTXXXKey("country", "COUNTRY") 51 | 52 | 53 | class Mp3(MediaBase): 54 | """A MP3 file object""" 55 | 56 | def __init__(self, newmedia): 57 | MediaBase.__init__(self) 58 | 59 | self.description = "MPEG audio Layer III" 60 | self.mime_type = 'audio/mpeg' 61 | self.extension = 'mp3' 62 | self.format = 'MP3' 63 | 64 | self.media = newmedia 65 | self.source = self.media 66 | self.bitrate_default = 192 67 | self.tagdata = { 68 | 'title': 'TIT2', 69 | 'artist': 'TPE1', 70 | 'album': 'TALB', 71 | 'date': 'TDRC', 72 | 'comment': 'COMM', 73 | 'country': 'COUNTRY', 74 | 'genre': 'TCON', 75 | 'copyright': 'TCOP' 76 | } 77 | self.sourceobj = MP3(self.media, ID3=EasyID3) 78 | self.info = self.sourceobj.info 79 | self.bitrate = self.bitrate_default 80 | try: 81 | self.bitrate = int(self.info.bitrate / 1024) 82 | except: 83 | pass 84 | 85 | self.media_info = get_file_info(self.media) 86 | self.file_name = self.media_info[0] 87 | self.file_title = self.media_info[1] 88 | self.file_ext = self.media_info[2] 89 | self.size = os.path.getsize(self.media) 90 | self.length = datetime.timedelta(0, self.info.length) 91 | self.read_file_metadata() 92 | 93 | def write_tags(self): 94 | """Write all ID3v2.4 tags by mapping dub2id3_dict dictionnary with the 95 | respect of mutagen classes and methods""" 96 | 97 | self.sourceobj.add_tags() 98 | self.sourceobj.tags['TIT2'] = id3.TIT2(encoding=2, text='text') 99 | self.sourceobj.save() 100 | 101 | ''' 102 | # media_id3 = id3.ID3(self.media) 103 | # for tag in self.metadata.keys(): 104 | # if tag in self.dub2id3_dict: 105 | # frame_text = self.dub2id3_dict[tag] 106 | # value = self.metadata[tag] 107 | # frame = mutagen.id3.Frames[frame_text](3,value) 108 | # try: 109 | # media_id3.add(frame) 110 | # except: 111 | # raise IOError('ExporterError: cannot tag "'+tag+'"') 112 | 113 | # try: 114 | # media_id3.save() 115 | # except: 116 | # raise IOError('ExporterError: cannot write tags') 117 | ''' 118 | 119 | media = id3.ID3(self.media) 120 | media.add(id3.TIT2(encoding=3, text=self.metadata['title'].decode('utf8'))) 121 | media.add(id3.TP1(encoding=3, text=self.metadata['artist'].decode('utf8'))) 122 | media.add(id3.TAL(encoding=3, text=self.metadata['album'].decode('utf8'))) 123 | media.add(id3.TDRC(encoding=3, text=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) 124 | media.add(id3.TCO(encoding=3, text=self.metadata['genre'].decode('utf8'))) 125 | try: 126 | media.save() 127 | except: 128 | raise IOError('ExporterError: cannot write tags') 129 | 130 | 131 | -------------------------------------------------------------------------------- /debian/manpage.sgml.ex: -------------------------------------------------------------------------------- 1 | manpage.1'. You may view 5 | the manual page with: `docbook-to-man manpage.sgml | nroff -man | 6 | less'. A typical entry in a Makefile or Makefile.am is: 7 | 8 | manpage.1: manpage.sgml 9 | docbook-to-man $< > $@ 10 | 11 | 12 | The docbook-to-man binary is found in the docbook-to-man package. 13 | Please remember that if you create the nroff version in one of the 14 | debian/rules file targets (such as build), you will need to include 15 | docbook-to-man in your Build-Depends control field. 16 | 17 | --> 18 | 19 | 20 | FIRSTNAME"> 21 | SURNAME"> 22 | 23 | décembre 9, 2007"> 24 | 26 | SECTION"> 27 | yomguy@altern.org"> 28 | 29 | D-FUZZ"> 30 | 31 | 32 | Debian"> 33 | GNU"> 34 | GPL"> 35 | ]> 36 | 37 | 38 | 39 |
40 | &dhemail; 41 |
42 | 43 | &dhfirstname; 44 | &dhsurname; 45 | 46 | 47 | 2003 48 | &dhusername; 49 | 50 | &dhdate; 51 |
52 | 53 | &dhucpackage; 54 | 55 | &dhsection; 56 | 57 | 58 | &dhpackage; 59 | 60 | program to do something 61 | 62 | 63 | 64 | &dhpackage; 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | DESCRIPTION 73 | 74 | This manual page documents briefly the 75 | &dhpackage; and bar 76 | commands. 77 | 78 | This manual page was written for the &debian; distribution 79 | because the original program does not have a manual page. 80 | Instead, it has documentation in the &gnu; 81 | Info format; see below. 82 | 83 | &dhpackage; is a program that... 84 | 85 | 86 | 87 | OPTIONS 88 | 89 | These programs follow the usual &gnu; command line syntax, 90 | with long options starting with two dashes (`-'). A summary of 91 | options is included below. For a complete description, see the 92 | Info files. 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | Show summary of options. 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | Show version of program. 109 | 110 | 111 | 112 | 113 | 114 | SEE ALSO 115 | 116 | bar (1), baz (1). 117 | 118 | The programs are documented fully by The Rise and 119 | Fall of a Fooish Bar available via the 120 | Info system. 121 | 122 | 123 | AUTHOR 124 | 125 | This manual page was written by &dhusername; &dhemail; for 126 | the &debian; system (but may be used by others). Permission is 127 | granted to copy, distribute and/or modify this document under 128 | the terms of the &gnu; General Public License, Version 2 any 129 | later version published by the Free Software Foundation. 130 | 131 | 132 | On Debian systems, the complete text of the GNU General Public 133 | License can be found in /usr/share/common-licenses/GPL. 134 | 135 | 136 | 137 |
138 | 139 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /deefuzzer/tools/mediabase.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Dennis Wallace' 2 | 3 | import tempfile 4 | 5 | class MediaBase(object): 6 | """Base Media class. All media objects should inherit from this class 7 | to allow common functions to be used in core code. See MP3 and OGG classes 8 | for examples on how to configure a subclass.""" 9 | 10 | def __init__(self): 11 | object.__init__(self) 12 | 13 | # Set the following five values in an inherited subclass. 14 | 15 | # A text string describing this media type 16 | self.description = '' 17 | 18 | # A text string declaring the MIME Type for this media type 19 | self.mime_type = '' 20 | 21 | # A text string declaring the common file extension for this media type 22 | self.extension = '' 23 | 24 | # A text string declaring the media format. The self.format property 25 | # should be unique across all subclasses inherited from MediaBase. 26 | self.format = '' 27 | 28 | # tagdata contains a dictionary of tags to use to gather metadata from the sourceobj 29 | self.tagdata = {} 30 | 31 | self.media = '' 32 | self.item_id = '' 33 | self.source = '' 34 | self.options = {} 35 | self.bitrate_default = 0 36 | self.info = {} 37 | self.bitrate = 0 38 | self.length = 0 39 | 40 | # sourceobj contains the metadata information for the referenced object 41 | self.sourceobj = {} 42 | 43 | self.media_info = [] 44 | self.file_name = '' 45 | self.file_title = '' 46 | self.file_ext = '' 47 | self.size = 0 48 | self.metadata = {} 49 | 50 | # A more cross-platform way to do this 51 | self.cache_dir = tempfile.gettempdir() 52 | 53 | def get_format(self): 54 | """Gets the format string of the media type""" 55 | return self.format 56 | 57 | def get_file_extension(self): 58 | """Gets the actual file extension string of the media""" 59 | return self.file_ext 60 | 61 | def get_mime_type(self): 62 | """Gets the MIME Type string for this media type""" 63 | return self.mime_type 64 | 65 | def get_description(self): 66 | """Gets the description string for this media type""" 67 | return self.description 68 | 69 | def set_cache_dir(self, path): 70 | """Sets an alternate location for temporary cache files used in this media object""" 71 | self.cache_dir = path 72 | 73 | def get_file_metadata(self, clear_cache=False): 74 | """Returns the metadata for the media, filtered by the tagdata dictionary for this media type. Return value is 75 | read from cache if possible (or unless clear_cache is set to True)""" 76 | if not self.metadata or clear_cache: 77 | self.read_file_metadata() 78 | return self.metadata 79 | 80 | def read_file_metadata(self): 81 | """Reads the metadata for the media, filtered by the tagdata dictionary for this media type""" 82 | self.metadata = {} 83 | for key in list(self.tagdata.keys()): 84 | self.metadata[key] = '' 85 | try: 86 | self.metadata[key] = self.sourceobj[key][0] 87 | except: 88 | pass 89 | 90 | try: 91 | if self.tagdata[key] != '' and self.metadata[key] == "": 92 | self.metadata[key] = self.sourceobj[self.tagdata[key]][0] 93 | except: 94 | pass 95 | 96 | def get_metadata_value(self, key, clean=False, clear_cache=False): 97 | """Returns a metadata value for a give key. If clean is True, then the resulting string will 98 | be cleaned before it is returned. If the key does not exist, an empty string is returned. Return 99 | value is read from cache if possible (or unless clear_cache is set to True)""" 100 | if not self.metadata or clear_cache: 101 | self.read_file_metadata() 102 | 103 | if key not in self.metadata: 104 | return '' 105 | r = self.metadata[key] 106 | if not r: 107 | r = ""; 108 | if clean: 109 | r = r.replace('_',' ').strip() 110 | return r 111 | 112 | def get_title(self): 113 | """Returns the cleaned title for this media""" 114 | return self.get_metadata_value('title', True) 115 | 116 | def get_artist(self): 117 | """Returns the cleaned artist for this media""" 118 | return self.get_metadata_value('artist', True) 119 | 120 | def get_song(self, usefn=True): 121 | """Returns a string in the form "artist - title" for this media. If either artist or title are blank, 122 | only the non-blank field is returned. If both fields are blank, and the usefn parameter is True, then 123 | the filename is returned instead. Otherwise, an empty string is returned.""" 124 | a = self.get_metadata_value('artist', True) 125 | t = self.get_metadata_value('title', True) 126 | if len(a) == 0 and len(t) == 0 and usefn: 127 | if self.file_name: 128 | a = self.file_name.encode('utf-8') 129 | r = a 130 | if len(a) > 0 and len(t) > 0: 131 | r += ' - ' 132 | r += t 133 | return r 134 | -------------------------------------------------------------------------------- /deefuzzer/tools/ogg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright Guillaume Pellerin (2006-2009) 5 | 6 | # 7 | 8 | # This software is a computer program whose purpose is to stream audio 9 | # and video data through icecast2 servers. 10 | 11 | # This software is governed by the CeCILL license under French law and 12 | # abiding by the rules of distribution of free software. You can use, 13 | # modify and/ or redistribute the software under the terms of the CeCILL 14 | # license as circulated by CEA, CNRS and INRIA at the following URL 15 | # "http://www.cecill.info". 16 | 17 | # As a counterpart to the access to the source code and rights to copy, 18 | # modify and redistribute granted by the license, users are provided only 19 | # with a limited warranty and the software's author, the holder of the 20 | # economic rights, and the successive licensors have only limited 21 | # liability. 22 | 23 | # In this respect, the user's attention is drawn to the risks associated 24 | # with loading, using, modifying and/or developing or reproducing the 25 | # software by the user in light of its specific status of free software, 26 | # that may mean that it is complicated to manipulate, and that also 27 | # therefore means that it is reserved for developers and experienced 28 | # professionals having in-depth computer knowledge. Users are therefore 29 | # encouraged to load and test the software's suitability as regards their 30 | # requirements in conditions enabling the security of their systems and/or 31 | # data to be ensured and, more generally, to use and operate it in the 32 | # same conditions as regards security. 33 | 34 | # The fact that you are presently reading this means that you have had 35 | # knowledge of the CeCILL license and that you accept its terms. 36 | 37 | # Author: Guillaume Pellerin 38 | 39 | import os 40 | import string 41 | import datetime 42 | from mutagen.oggvorbis import OggVorbis 43 | from .utils import * 44 | 45 | 46 | class Ogg(MediaBase): 47 | """An OGG file object""" 48 | 49 | def __init__(self, media): 50 | MediaBase.__init__(self) 51 | 52 | self.description = "OGG Vorbis" 53 | self.mime_type = 'audio/ogg' 54 | self.extension = 'ogg' 55 | self.format = 'OGG' 56 | 57 | self.media = media 58 | self.sourceobj = OggVorbis(self.media) 59 | self.source = self.media 60 | self.bitrate_default = '192' 61 | 62 | self.tagdata = { 63 | 'title': '', 64 | 'artist': '', 65 | 'album': '', 66 | 'date': '', 67 | 'comment': '', 68 | 'genre': '', 69 | 'copyright': '' 70 | } 71 | 72 | self.info = self.sourceobj.info 73 | self.bitrate = int(str(self.info.bitrate)[:-3]) 74 | self.length = datetime.timedelta(0, self.info.length) 75 | self.read_file_metadata() 76 | self.media_info = get_file_info(self.media) 77 | self.file_name = self.media_info[0] 78 | self.file_title = self.media_info[1] 79 | self.file_ext = self.media_info[2] 80 | self.size = os.path.getsize(self.media) 81 | 82 | def get_file_info(self): 83 | try: 84 | file_out1, file_out2 = os.popen4('ogginfo "' + self.source + '"') 85 | info = [] 86 | for line in file_out2.readlines(): 87 | info.append(clean_word(line[:-1])) 88 | self.info = info 89 | return self.info 90 | except: 91 | raise IOError('ExporterError: file does not exist.') 92 | 93 | def decode(self): 94 | if not self.item_id: 95 | raise IOError('ExporterError: Required item_id parameter not set.') 96 | try: 97 | p = os.path.join(self.cache_dir, (self.item_id + '.wav')) 98 | os.system('oggdec -o "' + p + '" "' + self.source + '"') 99 | return p 100 | except: 101 | raise IOError('ExporterError: decoder is not compatible.') 102 | 103 | def write_tags(self): 104 | # self.ogg.add_tags() 105 | for tag in list(self.metadata.keys()): 106 | self.sourceobj[tag] = str(self.metadata[tag]) 107 | self.sourceobj.save() 108 | 109 | def get_args(self, options=None): 110 | """Get process options and return arguments for the encoder""" 111 | args = [] 112 | if options is not None: 113 | self.options = options 114 | if not ('verbose' in self.options and self.options['verbose'] != '0'): 115 | args.append('-Q ') 116 | if 'ogg_bitrate' in self.options: 117 | args.append('-b ' + self.options['ogg_bitrate']) 118 | elif 'ogg_quality' in self.options: 119 | args.append('-q ' + self.options['ogg_quality']) 120 | else: 121 | args.append('-b ' + self.bitrate_default) 122 | else: 123 | args.append('-Q -b ' + self.bitrate_default) 124 | 125 | for tag in list(self.metadata.keys()): 126 | value = clean_word(self.metadata[tag]) 127 | args.append('-c %s="%s"' % (tag, value)) 128 | if tag in self.tagdata: 129 | arg = self.tagdata[tag] 130 | if arg: 131 | args.append('-c %s="%s"' % (arg, value)) 132 | 133 | return args 134 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/yomguy/DeeFuzzer/raw/master/doc/img/logo_deefuzzer.png 2 | 3 | |version| 4 | 5 | .. |version| image:: https://img.shields.io/pypi/v/DeeFuzzer.svg 6 | :target: https://pypi.python.org/pypi/DeeFuzzer/ 7 | :alt: Version 8 | 9 | DeeFuzzer is a light and instant application for streaming audio and video over internet. 10 | It is dedicated to communities who need to easily create web radios, web TVs, 11 | live multimedia relays or personal home radios, with metadata management and other cool features. 12 | 13 | 14 | Features 15 | ======== 16 | 17 | * Streaming MP3, OGG Vorbis files over Internet 18 | * Live streaming for any kind of format (WebM compatible) 19 | * Full metadata encapsulation and management 20 | * Recursive folders, random or M3U playlists management 21 | * M3U, RSS and JSON podcast generators for URLs, current tracks and playlists 22 | * Automagic mountpoint creation based on media subfolders 23 | * Multiple station streaming with only one config file 24 | * Auto twitting #nowplaying tracks 25 | * Auto jingling between tracks 26 | * OSC controller for a few commands 27 | * Very light and optimized streaming process 28 | * Fully written in Python 29 | * Works with Icecast2, ShoutCast, Stream-m 30 | * (NEW) Works with MySQL playlists 31 | 32 | Because our aim is to get DeeFuzzer as light as possible it is NOT capable of re-encoding or transcoding media files for the moment. 33 | 34 | 35 | News 36 | ==== 37 | 38 | 0.9.0 39 | 40 | * Python 3.11 compatibility 41 | * Add microseconds to rec filename 42 | * Add write_conf method to export config 43 | * Add version to logging 44 | 45 | 0.8.3 46 | 47 | * Bufix against libshout 2.4.6 48 | * Add more examples 49 | 50 | 0.8.0 51 | 52 | * Compatible with Python 3.7 (only) 53 | * Update dependencies 54 | * Update headers against GPL v3 licence 55 | 56 | 0.7.4 57 | 58 | * Change libmysqlclient-dev for libmariadbclient-dev 59 | 60 | 0.7.3 61 | 62 | * Add absolute feed_dir for feeds 63 | 64 | 0.7.2 65 | 66 | * Add MySQL module and connection routine to get the playlist from a database (thanks to doomy23) 67 | * Prepare the Python3 switch 68 | * Tested against libshout 2.4.1 and python-shout 0.2.5 69 | * As been used in relay mode in production for almost 5000+ hours 70 | * Improve conf YAML format support 71 | 72 | 0.7.1 73 | 74 | * Bugfix release 75 | * Fix no metadata for stream-m relaying 76 | 77 | 0.7 78 | 79 | * **Huge** refactoring which should be compatible with old setups, but before updating **please read** the `updated example `_ and the following news. 80 | * Reworked the RSS feed handling to allow JSON output as well and more configuration options (@achbed #27 #28) 81 | * Add an init.d script to act as a deamon (@achbed) 82 | * Add stationdefaults preference (apply default settings to all stations) (@achbed #31) 83 | * Add stationfolder preference (generate stations automatically from a folder structure) (@achbed #31) 84 | * Add stationconfig preference (load other preference files as stations) (@achbed #31) 85 | * Add new station.server.appendtype option 86 | * Add new base_dir parameter to station definition 87 | * Better thread management (@achbed #36 #37 #38) 88 | * Improved stability avoiding crashes with automatic station restart methods (@achbed #39 #45) 89 | * Added option (ignoreerrors) to log and continue when an error occurs during station initialization (@achbed #43) 90 | * Cleanup, better documentation and good ideas (@ChoiZ #15 #16 #17 #23) 91 | * Various bugfixes 92 | * Many thanks to all participants and especially to @achbed for his **huge** work, efficiency and easy collaboration 93 | * Enjoy! 94 | 95 | 0.6.6 96 | 97 | * Update station name (remove ": http://url") 98 | * Update mountpoint name (remove .mp3 or .ogg) 99 | * Update metadata (replace " : " by " - " between Artist and Track) 100 | * Remove "ogg_quality" on mp3 streams 101 | 102 | 0.6.5 103 | 104 | * Stable WebM live streaming through Stream-m server 105 | * Read yaml configuration files 106 | * Read m3u playlist files 107 | * Minor fixes 108 | 109 | 110 | Installation 111 | ============ 112 | 113 | DeeFuzzer has now only been well tested on Linux, but should work on any other platform. 114 | You would then need to install libshout3 and liblo libraries for it. On Windows, 115 | an install inside Gygwin should work well. 116 | 117 | To install it, say on Debian, do:: 118 | 119 | sudo apt-get install python3-pip python3-dev cython3 python3-liblo \ 120 | python3-mutagen python3-pycurl python3-yaml \ 121 | python3-mysqldb libshout3-dev librtmp-dev liblo-dev \ 122 | libcurl4-openssl-dev libmariadb-dev-compat 123 | 124 | Then:: 125 | 126 | sudo pip3 install deefuzzer 127 | 128 | Or to upgrade:: 129 | 130 | sudo pip3 install -U deefuzzer 131 | 132 | If you have some version problems with the installation, please also try in a virtualenv. 133 | 134 | As a streaming client, the DeeFuzzer needs a local or remote streaming server like Icecast2 to do something:: 135 | 136 | sudo apt-get install icecast2 137 | 138 | 139 | Usage 140 | ===== 141 | 142 | deefuzzer CONFIGFILE 143 | 144 | where CONFIGFILE is the path for a XML or YAML config file. For example:: 145 | 146 | deefuzzer example/deefuzzer.xml 147 | 148 | or:: 149 | 150 | deefuzzer example/deefuzzer.yaml 151 | 152 | To make the deefuzzer act as a deamon, just play it in the background:: 153 | 154 | deefuzzer example/deefuzzer.yaml & 155 | 156 | Note that you must edit the config file with right parameters before playing. 157 | 158 | 159 | Documentation 160 | ============= 161 | 162 | * `FAQ and Wiki `_ 163 | * `API `_ 164 | * `Documented XML configuration `_ 165 | * Configuration examples: 166 | 167 | * `Dummy XML for testing `_ 168 | * `Generic YAML `_ 169 | 170 | 171 | Development 172 | =========== 173 | 174 | Everybody is welcome to participate to the DeeFuzzer project! 175 | 176 | We use GitHub to collaborate: https://github.com/yomguy/DeeFuzzer 177 | 178 | Clone it, star it and join us! 179 | 180 | 181 | Authors 182 | ======= 183 | 184 | * @yomguy +GuillaumePellerin yomguy@parisson.com 185 | * @achbed +achbed github@achbed.org 186 | * @ChoiZ +FrançoisLASSERRE choiz@me.com 187 | 188 | 189 | License 190 | ======= 191 | 192 | This software is released under the terms of the GNU GPL v3. 193 | as described in the file LICENSE.txt in the source directory or online https://github.com/yomguy/DeeFuzzer/blob/master/LICENSE.txt 194 | 195 | 196 | Aknowledgements 197 | =============== 198 | 199 | This work is inspired by the great - C coded - Oddsock's streaming program: Ezstream. 200 | Since I needed to patch it in order to modify the playlist (randomize for example) 201 | and make external batch tools to create multiple channels, I decided to rewrite it 202 | from scratch in python. 203 | 204 | Some parts of this work are also taken from another Parisson's project: Telemeta 205 | (see http://telemeta.org). 206 | 207 | -------------------------------------------------------------------------------- /example/deefuzzer_doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | /path/to/station.log 7 | 10 | /path/to/station.m3u 11 | 13 | 0 14 | 18 | 0 19 | 20 | 24 | 25 | 27 | 0 28 | 29 | 16001 30 | 31 | 32 | 34 | /path/to/jingles 35 | 36 | 0 37 | 38 | 1 39 | 40 | 41 | 42 | 55 | /path/to/station/folder 56 | 57 | 59 | 0 60 | 61 | 16001 62 | 63 | 64 | 65 | My personal best funky playlist ever! 66 | 67 | My best funky station 68 | 70 | My_station 71 | 72 | http://parisson.com 73 | 74 | Various Funk Groove 75 | 76 | 77 | 79 | /path/to/jingles 80 | 82 | 0 83 | 84 | 1 85 | 86 | 87 | 88 | 96 89 | 90 | /path/to/m3u_file 91 | 93 | /path/to/mp3_folder 94 | 97 | /path/to/m3u_file_or_folder 98 | 99 | mp3 100 | 101 | 4 102 | 103 | 48000 104 | 105 | 0 106 | 107 | 2 108 | 109 | 110 | 111 | /path/to/archives 112 | 113 | 0 114 | 115 | 116 | 117 | Unknown 118 | 120 | 0 121 | 122 | http://127.0.0.1:8000/telecaster_live.mp3 123 | 124 | 125 | 126 | 1 127 | 128 | 1 129 | 130 | 0 131 | 132 | 1 133 | 137 | /path/to/rss/ 138 | 140 | 1 141 | 143 | http://localhost/media/ 144 | 145 | 1 146 | 147 | 0 148 | 149 | 150 | 151 | 127.0.0.1 152 | 153 | monitor 154 | 155 | 8000 156 | 157 | 0 158 | 159 | icecast_source_password 160 | 161 | icecast 162 | 164 | 0 165 | 166 | 167 | 168 | your access token key 169 | 170 | 0 171 | 172 | your access token secret key 173 | 174 | parisson deefuzzer 175 | 176 | 177 | 178 | 179 | 180 | 182 | 183 | 184 | /path/to/media 185 | 187 | 1 188 | 191 | 192 | [name] 193 | [name] 194 | [name] 195 | 196 | 197 | 198 | 200 | /path/to/configs 201 | /path/to/configs2 202 | 203 | -------------------------------------------------------------------------------- /deefuzzer/core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2006-2011 Guillaume Pellerin 5 | 6 | # 7 | 8 | # This file is part of deefuzzer 9 | 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | 25 | import os 26 | import shout 27 | import queue 28 | import datetime 29 | import mimetypes 30 | import hashlib 31 | import platform 32 | from threading import Thread 33 | 34 | from .station import * 35 | from .tools import * 36 | 37 | from .version import __version__ 38 | 39 | mimetypes.add_type('application/x-yaml', '.yaml') 40 | mimetypes.add_type('application/x-yaml', '.yml') 41 | 42 | 43 | class DeeFuzzer(Thread): 44 | """a DeeFuzzer diffuser""" 45 | 46 | logger = None 47 | m3u = None 48 | rss = None 49 | station_settings = [] 50 | station_instances = {} 51 | watch_folder = {} 52 | log_queue = queue.Queue() 53 | main_loop = False 54 | ignore_errors = False 55 | max_retry = 0 56 | 57 | def __init__(self, args): 58 | Thread.__init__(self) 59 | self.config_file = args.config_file 60 | self.conf = get_conf_dict(self.config_file) 61 | # print(self.conf) 62 | 63 | if 'deefuzzer' not in self.conf : 64 | raise('This is not a standard deefuzzer config file') 65 | 66 | # Get the log setting first (if possible) 67 | log_file = str(self.conf['deefuzzer'].pop('log', '')) 68 | self.log_dir = os.sep.join(log_file.split(os.sep)[:-1]) 69 | if not os.path.exists(self.log_dir) and self.log_dir: 70 | os.makedirs(self.log_dir) 71 | self.logger = QueueLogger(log_file, self.log_queue) 72 | self.logger.start() 73 | 74 | for key in list(self.conf['deefuzzer'].keys()): 75 | if key == 'm3u': 76 | self.m3u = str(self.conf['deefuzzer'][key]) 77 | 78 | elif key == 'ignoreerrors': 79 | # Ignore errors and continue as long as possible 80 | self.ignore_errors = bool(self.conf['deefuzzer'][key]) 81 | 82 | elif key == 'max_retry': 83 | # Maximum number of attempts to restart the stations on crash. 84 | self.max_retry = int(self.conf['deefuzzer'][key]) 85 | 86 | elif key == 'station': 87 | # Load station definitions from the main config file 88 | if not isinstance(self.conf['deefuzzer'][key], list): 89 | self.add_station(self.conf['deefuzzer'][key]) 90 | else: 91 | for s in self.conf['deefuzzer'][key]: 92 | self.add_station(s) 93 | 94 | elif key == 'stationconfig': 95 | # Load additional station definitions from the requested folder 96 | self.load_stations_fromconfig(self.conf['deefuzzer'][key]) 97 | 98 | elif key == 'stationfolder': 99 | # Create stations automagically from a folder structure 100 | if isinstance(self.conf['deefuzzer'][key], dict): 101 | self.watch_folder = self.conf['deefuzzer'][key] 102 | else: 103 | setattr(self, key, self.conf['deefuzzer'][key]) 104 | 105 | # Set the deefuzzer logger 106 | self._info('Starting DeeFuzzer version %s' % __version__) 107 | self._info('Using Python version %s' % platform.python_version()) 108 | self._info('Using libshout version %s' % shout.version()) 109 | self._info('Number of stations : ' + str(len(self.station_settings))) 110 | 111 | def _log(self, level, msg): 112 | try: 113 | obj = {'msg': 'Core: ' + str(msg), 'level': level} 114 | self.log_queue.put(obj) 115 | except: 116 | pass 117 | 118 | def _info(self, msg): 119 | self._log('info', msg) 120 | 121 | def _err(self, msg): 122 | self._log('err', msg) 123 | 124 | def set_m3u_playlist(self): 125 | m3u_dir = os.sep.join(self.m3u.split(os.sep)[:-1]) 126 | if not os.path.exists(m3u_dir) and m3u_dir: 127 | os.makedirs(m3u_dir) 128 | m3u = open(self.m3u, 'w') 129 | m3u.write('#EXTM3U\n') 130 | for station in self.station_settings: 131 | m3u.write('#EXTINF:%s,%s\n' % ('-1', station['infos']['name'])) 132 | m3u.write('http://' + station['server']['host'] + ':' + \ 133 | str(station['server']['port']) + '/' + station['server']['mountpoint'] + '\n') 134 | m3u.close() 135 | self._info('Writing M3U file to : ' + self.m3u) 136 | 137 | def create_stations_fromfolder(self): 138 | """Scan a folder for subfolders containing media, and make stations from them all.""" 139 | 140 | options = self.watch_folder 141 | if 'folder' not in options: 142 | # We have no folder specified. Bail. 143 | return 144 | 145 | if self.main_loop: 146 | if 'livecreation' not in options: 147 | # We have no folder specified. Bail. 148 | return 149 | 150 | if int(options['livecreation']) == 0: 151 | # Livecreation not specified. Bail. 152 | return 153 | 154 | folder = str(options['folder']) 155 | if not os.path.isdir(folder): 156 | # The specified path is not a folder. Bail. 157 | return 158 | 159 | # This makes the log file a lot more verbose. Commented out since we report on new stations anyway. 160 | # self._info('Scanning folder ' + folder + ' for stations') 161 | 162 | if 'infos' not in options: 163 | options['infos'] = {} 164 | if 'short_name' not in options['infos']: 165 | options['infos']['short_name'] = '[name]' 166 | 167 | files = os.listdir(folder) 168 | for file in files: 169 | filepath = os.path.join(folder, file) 170 | if os.path.isdir(filepath): 171 | if folder_contains_music(filepath): 172 | self.create_station(filepath, options) 173 | 174 | def station_exists(self, name): 175 | try: 176 | for s in self.station_settings: 177 | if 'infos' not in s: 178 | continue 179 | if 'short_name' not in s['infos']: 180 | continue 181 | if s['infos']['short_name'] == name: 182 | return True 183 | return False 184 | except: 185 | pass 186 | return True 187 | 188 | def create_station(self, folder, options): 189 | """Create a station definition for a folder given the specified options.""" 190 | 191 | s = {} 192 | path, name = os.path.split(folder) 193 | if self.station_exists(name): 194 | return 195 | self._info('Creating station for folder ' + folder) 196 | d = dict(path=folder, name=name) 197 | for i in list(options.keys()): 198 | if 'folder' not in i: 199 | s[i] = replace_all(options[i], d) 200 | if 'media' not in s: 201 | s['media'] = {} 202 | s['media']['source'] = folder 203 | 204 | self.add_station(s) 205 | 206 | def load_stations_fromconfig(self, folder): 207 | """Load one or more configuration files looking for stations.""" 208 | 209 | if isinstance(folder, dict) or isinstance(folder, list): 210 | # We were given a list or dictionary. Loop though it and load em all 211 | for f in folder: 212 | self.load_station_configs(f) 213 | return 214 | 215 | if os.path.isfile(folder): 216 | # We have a file specified. Load just that file. 217 | self.load_station_config(folder) 218 | return 219 | 220 | if not os.path.isdir(folder): 221 | # Whatever we have, it's not either a file or folder. Bail. 222 | return 223 | 224 | self._info('Loading station config files in ' + folder) 225 | files = os.listdir(folder) 226 | for file in files: 227 | filepath = os.path.join(folder, file) 228 | if os.path.isfile(filepath): 229 | self.load_station_config(filepath) 230 | 231 | def load_station_config(self, file): 232 | """Load station configuration(s) from a config file.""" 233 | 234 | self._info('Loading station config file ' + file) 235 | stationdef = get_conf_dict(file) 236 | if isinstance(stationdef, dict): 237 | if 'station' in stationdef: 238 | if isinstance(stationdef['station'], dict): 239 | self.add_station(stationdef['station']) 240 | elif isinstance(stationdef['station'], list): 241 | for s in stationdef['station']: 242 | self.add_station(s) 243 | 244 | def add_station(self, this_station): 245 | """Adds a station configuration to the list of stations.""" 246 | try: 247 | # We should probably test to see if we're putting the same station in multiple times 248 | # Same in this case probably means the same media folder, server, and mountpoint 249 | self.station_settings.append(this_station) 250 | except Exception: 251 | return 252 | 253 | def run(self): 254 | q = queue.Queue(1) 255 | ns = 0 256 | p = Producer(q) 257 | p.start() 258 | # Keep the Stations running 259 | while True: 260 | self.create_stations_fromfolder() 261 | ns_new = len(self.station_settings) 262 | if ns_new > ns: 263 | self._info('Loading new stations') 264 | 265 | for i in range(0, ns_new): 266 | name = '' 267 | try: 268 | if 'station_name' in self.station_settings[i]: 269 | name = self.station_settings[i]['station_name'] 270 | 271 | if 'retries' not in self.station_settings[i]: 272 | self.station_settings[i]['retries'] = 0 273 | 274 | try: 275 | if 'station_instance' in self.station_settings[i]: 276 | # Check for station running here 277 | if self.station_settings[i]['station_instance'].is_alive: 278 | # Station exists and is alive. Don't recreate. 279 | self.station_settings[i]['retries'] = 0 280 | continue 281 | 282 | if self.max_retry >= 0 and self.station_settings[i]['retries'] <= self.max_retry: 283 | # Station passed the max retries count is will not be reloaded 284 | if 'station_stop_logged' not in self.station_settings[i]: 285 | self._err('Station ' + name + ' is stopped and will not be restarted.') 286 | self.station_settings[i]['station_stop_logged'] = True 287 | continue 288 | 289 | self.station_settings[i]['retries'] += 1 290 | trynum = str(self.station_settings[i]['retries']) 291 | self._info('Restarting station ' + name + ' (try ' + trynum + ')') 292 | except Exception as e: 293 | self._err('Error checking status for ' + name) 294 | self._err(str(e)) 295 | if not self.ignore_errors: 296 | raise 297 | 298 | # Apply station defaults if they exist 299 | if 'stationdefaults' in self.conf['deefuzzer']: 300 | if isinstance(self.conf['deefuzzer']['stationdefaults'], dict): 301 | self.station_settings[i] = merge_defaults( 302 | self.station_settings[i], 303 | self.conf['deefuzzer']['stationdefaults'] 304 | ) 305 | 306 | if name == '': 307 | name = 'Station ' + str(i) 308 | if 'info' in self.station_settings[i]: 309 | if 'short_name' in self.station_settings[i]['infos']: 310 | name = self.station_settings[i]['infos']['short_name'] 311 | y = 1 312 | while name in list(self.station_instances.keys()): 313 | y += 1 314 | name = self.station_settings[i]['infos']['short_name'] + " " + str(y) 315 | 316 | self.station_settings[i]['station_name'] = name 317 | namehash = hashlib.md5(str(name).encode('utf-8')).hexdigest() 318 | self.station_settings[i]['station_statusfile'] = os.sep.join([self.log_dir, namehash]) 319 | 320 | new_station = Station(self.station_settings[i], q, self.log_queue, self.m3u) 321 | if new_station.valid: 322 | self.station_settings[i]['station_instance'] = new_station 323 | self.station_settings[i]['station_instance'].start() 324 | self._info('Started station ' + name) 325 | else: 326 | self._err('Error validating station ' + name) 327 | except Exception: 328 | self._err('Error initializing station ' + name) 329 | if not self.ignore_errors: 330 | raise 331 | continue 332 | 333 | if self.m3u: 334 | self.set_m3u_playlist() 335 | 336 | ns = ns_new 337 | self.main_loop = True 338 | 339 | time.sleep(5) 340 | # end main loop 341 | 342 | 343 | class Producer(Thread): 344 | """a DeeFuzzer Producer master thread. Used for locking/blocking""" 345 | 346 | def __init__(self, q): 347 | Thread.__init__(self) 348 | self.q = q 349 | 350 | def run(self): 351 | while True: 352 | try: 353 | self.q.put(True, True) 354 | except: 355 | pass 356 | -------------------------------------------------------------------------------- /deefuzzer/tools/PyRSS2Gen.py: -------------------------------------------------------------------------------- 1 | """PyRSS2Gen - A Python library for generating RSS 2.0 feeds.""" 2 | 3 | __name__ = "PyRSS2Gen" 4 | __version__ = (1, 0, 0) 5 | __author__ = "Andrew Dalke " 6 | 7 | _generator_name = __name__ + "-" + ".".join(map(str, __version__)) 8 | 9 | import datetime 10 | 11 | 12 | # Could make this the base class; will need to add 'publish' 13 | class WriteXmlMixin: 14 | def __init__(self): 15 | pass 16 | 17 | def write_xml(self, outfile, encoding="iso-8859-1"): 18 | from xml.sax import saxutils 19 | 20 | handler = saxutils.XMLGenerator(outfile, encoding) 21 | handler.startDocument() 22 | self.publish(handler) 23 | handler.endDocument() 24 | 25 | def to_xml(self, encoding="iso-8859-1"): 26 | try: 27 | import io as StringIO 28 | except ImportError: 29 | import io 30 | f = io.StringIO() 31 | self.write_xml(f, encoding) 32 | return f.getvalue() 33 | 34 | 35 | def _element(handler, name, obj, d=None): 36 | if not d: 37 | d = {} 38 | if isinstance(obj, str) or obj is None: 39 | # special-case handling to make the API easier 40 | # to use for the common case. 41 | handler.startElement(name, d) 42 | if obj is not None: 43 | handler.characters(obj) 44 | handler.endElement(name) 45 | else: 46 | # It better know how to emit the correct XML. 47 | obj.publish(handler) 48 | 49 | 50 | def _opt_element(handler, name, obj): 51 | if obj is None: 52 | return 53 | _element(handler, name, obj) 54 | 55 | 56 | def _format_date(dt): 57 | """convert a datetime into an RFC 822 formatted date 58 | 59 | Input date must be in GMT. 60 | """ 61 | # Looks like: 62 | # Sat, 07 Sep 2002 00:00:01 GMT 63 | # Can't use strftime because that's locale dependent 64 | # 65 | # Isn't there a standard way to do this for Python? The 66 | # rfc822 and email.Utils modules assume a timestamp. The 67 | # following is based on the rfc822 module. 68 | return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( 69 | ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()], 70 | dt.day, 71 | ["Jan", "Feb", "Mar", "Apr", "May", "Jun", 72 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][dt.month - 1], 73 | dt.year, dt.hour, dt.minute, dt.second) 74 | 75 | 76 | ## 77 | # A couple simple wrapper objects for the fields which 78 | # take a simple value other than a string. 79 | class IntElement: 80 | """implements the 'publish' API for integers 81 | 82 | Takes the tag name and the integer value to publish. 83 | 84 | (Could be used for anything which uses str() to be published 85 | to text for XML.) 86 | """ 87 | element_attrs = {} 88 | 89 | def __init__(self, name, val): 90 | self.name = name 91 | self.val = val 92 | 93 | def publish(self, handler): 94 | handler.startElement(self.name, self.element_attrs) 95 | handler.characters(str(self.val)) 96 | handler.endElement(self.name) 97 | 98 | 99 | class DateElement: 100 | """implements the 'publish' API for a datetime.datetime 101 | 102 | Takes the tag name and the datetime to publish. 103 | 104 | Converts the datetime to RFC 2822 timestamp (4-digit year). 105 | """ 106 | 107 | def __init__(self, name, dt): 108 | self.name = name 109 | self.dt = dt 110 | 111 | def publish(self, handler): 112 | _element(handler, self.name, _format_date(self.dt)) 113 | 114 | 115 | #### 116 | 117 | class Category: 118 | """Publish a category element""" 119 | 120 | def __init__(self, category, domain=None): 121 | self.category = category 122 | self.domain = domain 123 | 124 | def publish(self, handler): 125 | d = {} 126 | if self.domain is not None: 127 | d["domain"] = self.domain 128 | _element(handler, "category", self.category, d) 129 | 130 | 131 | class Cloud: 132 | """Publish a cloud""" 133 | 134 | def __init__(self, domain, port, path, 135 | registerProcedure, protocol): 136 | self.domain = domain 137 | self.port = port 138 | self.path = path 139 | self.registerProcedure = registerProcedure 140 | self.protocol = protocol 141 | 142 | def publish(self, handler): 143 | _element(handler, "cloud", None, { 144 | "domain": self.domain, 145 | "port": str(self.port), 146 | "path": self.path, 147 | "registerProcedure": self.registerProcedure, 148 | "protocol": self.protocol}) 149 | 150 | 151 | class Image: 152 | """Publish a channel Image""" 153 | element_attrs = {} 154 | 155 | def __init__(self, url, title, link, 156 | width=None, height=None, description=None): 157 | self.url = url 158 | self.title = title 159 | self.link = link 160 | self.width = width 161 | self.height = height 162 | self.description = description 163 | 164 | def publish(self, handler): 165 | handler.startElement("image", self.element_attrs) 166 | 167 | _element(handler, "url", self.url) 168 | _element(handler, "title", self.title) 169 | _element(handler, "link", self.link) 170 | 171 | width = self.width 172 | if isinstance(width, int): 173 | width = IntElement("width", width) 174 | _opt_element(handler, "width", width) 175 | 176 | height = self.height 177 | if isinstance(height, int): 178 | height = IntElement("height", height) 179 | _opt_element(handler, "height", height) 180 | 181 | _opt_element(handler, "description", self.description) 182 | 183 | handler.endElement("image") 184 | 185 | 186 | class Guid: 187 | """Publish a guid 188 | 189 | Defaults to being a permalink, which is the assumption if it's 190 | omitted. Hence strings are always permalinks. 191 | """ 192 | 193 | def __init__(self, guid, isPermaLink=1): 194 | self.guid = guid 195 | self.isPermaLink = isPermaLink 196 | 197 | def publish(self, handler): 198 | d = {} 199 | if self.isPermaLink: 200 | d["isPermaLink"] = "true" 201 | else: 202 | d["isPermaLink"] = "false" 203 | _element(handler, "guid", self.guid, d) 204 | 205 | 206 | class TextInput: 207 | """Publish a textInput 208 | 209 | Apparently this is rarely used. 210 | """ 211 | element_attrs = {} 212 | 213 | def __init__(self, title, description, name, link): 214 | self.title = title 215 | self.description = description 216 | self.name = name 217 | self.link = link 218 | 219 | def publish(self, handler): 220 | handler.startElement("textInput", self.element_attrs) 221 | _element(handler, "title", self.title) 222 | _element(handler, "description", self.description) 223 | _element(handler, "name", self.name) 224 | _element(handler, "link", self.link) 225 | handler.endElement("textInput") 226 | 227 | 228 | class Enclosure: 229 | """Publish an enclosure""" 230 | 231 | def __init__(self, url, length, type): 232 | self.url = url 233 | self.length = length 234 | self.type = type 235 | 236 | def publish(self, handler): 237 | _element(handler, "enclosure", None, { 238 | "url": self.url, 239 | "length": str(self.length), 240 | "type": self.type 241 | }) 242 | 243 | 244 | class Source: 245 | """Publish the item's original source, used by aggregators""" 246 | 247 | def __init__(self, name, url): 248 | self.name = name 249 | self.url = url 250 | 251 | def publish(self, handler): 252 | _element(handler, "source", self.name, {"url": self.url}) 253 | 254 | 255 | class SkipHours: 256 | """Publish the skipHours 257 | 258 | This takes a list of hours, as integers. 259 | """ 260 | element_attrs = {} 261 | 262 | def __init__(self, hours): 263 | self.hours = hours 264 | 265 | def publish(self, handler): 266 | if self.hours: 267 | handler.startElement("skipHours", self.element_attrs) 268 | for hour in self.hours: 269 | _element(handler, "hour", str(hour)) 270 | handler.endElement("skipHours") 271 | 272 | 273 | class SkipDays: 274 | """Publish the skipDays 275 | 276 | This takes a list of days as strings. 277 | """ 278 | element_attrs = {} 279 | 280 | def __init__(self, days): 281 | self.days = days 282 | 283 | def publish(self, handler): 284 | if self.days: 285 | handler.startElement("skipDays", self.element_attrs) 286 | for day in self.days: 287 | _element(handler, "day", day) 288 | handler.endElement("skipDays") 289 | 290 | 291 | class RSS2(WriteXmlMixin): 292 | """The main RSS class. 293 | 294 | Stores the channel attributes, with the "category" elements under 295 | ".categories" and the RSS items under ".items". 296 | """ 297 | 298 | rss_attrs = {"version": "2.0"} 299 | element_attrs = {} 300 | 301 | def __init__( 302 | self, 303 | title, 304 | link, 305 | description, 306 | 307 | language=None, 308 | copyright=None, 309 | managingEditor=None, 310 | webMaster=None, 311 | pubDate=None, # a datetime, *in* *GMT* 312 | lastBuildDate=None, # a datetime 313 | 314 | categories=None, # list of strings or Category 315 | generator=_generator_name, 316 | docs="http://blogs.law.harvard.edu/tech/rss", 317 | cloud=None, # a Cloud 318 | ttl=None, # integer number of minutes 319 | 320 | image=None, # an Image 321 | rating=None, # a string; I don't know how it's used 322 | textInput=None, # a TextInput 323 | skipHours=None, # a SkipHours with a list of integers 324 | skipDays=None, # a SkipDays with a list of strings 325 | items=None # list of RSSItems 326 | ): 327 | self.title = title 328 | self.link = link 329 | self.description = description 330 | self.language = language 331 | self.copyright = copyright 332 | self.managingEditor = managingEditor 333 | 334 | self.webMaster = webMaster 335 | self.pubDate = pubDate 336 | self.lastBuildDate = lastBuildDate 337 | 338 | if categories is None: 339 | categories = [] 340 | self.categories = categories 341 | self.generator = generator 342 | self.docs = docs 343 | self.cloud = cloud 344 | self.ttl = ttl 345 | self.image = image 346 | self.rating = rating 347 | self.textInput = textInput 348 | self.skipHours = skipHours 349 | self.skipDays = skipDays 350 | 351 | if items is None: 352 | items = [] 353 | self.items = items 354 | 355 | def publish(self, handler): 356 | handler.startElement("rss", self.rss_attrs) 357 | handler.startElement("channel", self.element_attrs) 358 | _element(handler, "title", self.title) 359 | _element(handler, "link", self.link) 360 | _element(handler, "description", self.description) 361 | 362 | self.publish_extensions(handler) 363 | 364 | _opt_element(handler, "language", self.language) 365 | _opt_element(handler, "copyright", self.copyright) 366 | _opt_element(handler, "managingEditor", self.managingEditor) 367 | _opt_element(handler, "webMaster", self.webMaster) 368 | 369 | pubDate = self.pubDate 370 | if isinstance(pubDate, datetime.datetime): 371 | pubDate = DateElement("pubDate", pubDate) 372 | _opt_element(handler, "pubDate", pubDate) 373 | 374 | lastBuildDate = self.lastBuildDate 375 | if isinstance(lastBuildDate, datetime.datetime): 376 | lastBuildDate = DateElement("lastBuildDate", lastBuildDate) 377 | _opt_element(handler, "lastBuildDate", lastBuildDate) 378 | 379 | for category in self.categories: 380 | if isinstance(category, str): 381 | category = Category(category) 382 | category.publish(handler) 383 | 384 | _opt_element(handler, "generator", self.generator) 385 | _opt_element(handler, "docs", self.docs) 386 | 387 | if self.cloud is not None: 388 | self.cloud.publish(handler) 389 | 390 | ttl = self.ttl 391 | if isinstance(self.ttl, int): 392 | ttl = IntElement("ttl", ttl) 393 | _opt_element(handler, "tt", ttl) 394 | 395 | if self.image is not None: 396 | self.image.publish(handler) 397 | 398 | _opt_element(handler, "rating", self.rating) 399 | if self.textInput is not None: 400 | self.textInput.publish(handler) 401 | if self.skipHours is not None: 402 | self.skipHours.publish(handler) 403 | if self.skipDays is not None: 404 | self.skipDays.publish(handler) 405 | 406 | for item in self.items: 407 | item.publish(handler) 408 | 409 | handler.endElement("channel") 410 | handler.endElement("rss") 411 | 412 | def publish_extensions(self, handler): 413 | # Derived classes can hook into this to insert 414 | # output after the three required fields. 415 | pass 416 | 417 | 418 | class RSSItem(WriteXmlMixin): 419 | """Publish an RSS Item""" 420 | element_attrs = {} 421 | 422 | def __init__( 423 | self, 424 | title=None, # string 425 | link=None, # url as string 426 | description=None, # string 427 | author=None, # email address as string 428 | categories=None, # list of string or Category 429 | comments=None, # url as string 430 | enclosure=None, # an Enclosure 431 | guid=None, # a unique string 432 | pubDate=None, # a datetime 433 | source=None # a Source 434 | ): 435 | 436 | if title is None and description is None: 437 | raise TypeError( 438 | "must define at least one of 'title' or 'description'") 439 | self.title = title 440 | self.link = link 441 | self.description = description 442 | self.author = author 443 | if categories is None: 444 | categories = [] 445 | self.categories = categories 446 | self.comments = comments 447 | self.enclosure = enclosure 448 | self.guid = guid 449 | self.pubDate = pubDate 450 | self.source = source 451 | # It sure does get tedious typing these names three times... 452 | 453 | def publish(self, handler): 454 | handler.startElement("item", self.element_attrs) 455 | _opt_element(handler, "title", self.title) 456 | _opt_element(handler, "link", self.link) 457 | self.publish_extensions(handler) 458 | _opt_element(handler, "description", self.description) 459 | _opt_element(handler, "author", self.author) 460 | 461 | for category in self.categories: 462 | if isinstance(category, str): 463 | category = Category(category) 464 | category.publish(handler) 465 | 466 | _opt_element(handler, "comments", self.comments) 467 | if self.enclosure is not None: 468 | self.enclosure.publish(handler) 469 | _opt_element(handler, "guid", self.guid) 470 | 471 | pubDate = self.pubDate 472 | if isinstance(pubDate, datetime.datetime): 473 | pubDate = DateElement("pubDate", pubDate) 474 | _opt_element(handler, "pubDate", pubDate) 475 | 476 | if self.source is not None: 477 | self.source.publish(handler) 478 | 479 | handler.endElement("item") 480 | 481 | def publish_extensions(self, handler): 482 | # Derived classes can hook into this to insert 483 | # output after the title and link elements 484 | pass 485 | -------------------------------------------------------------------------------- /deefuzzer/tools/xmltodict2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ xmltodict(): convert xml into tree of Python dicts. 3 | 4 | This was copied and modified from John Bair's recipe at aspn.activestate.com: 5 | http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/149368 6 | """ 7 | import os 8 | import string 9 | import locale 10 | from xml.parsers import expat 11 | 12 | """ """ 13 | 14 | ''' 15 | # If we're in Dabo, get the default encoding. 16 | # import dabo 17 | # import dabo.lib.DesignerUtils as desUtil 18 | # from dabo.dLocalize import _ 19 | # from dabo.lib.utils import resolvePath 20 | # app = dabo.dAppRef 21 | # if app is not None: 22 | # default_encoding = app.Encoding 23 | # else: 24 | # enc = locale.getlocale()[1] 25 | # if enc is None: 26 | # enc = dabo.defaultEncoding 27 | # default_encoding = enc 28 | ''' 29 | 30 | # Python seems to need to compile code with \n linesep: 31 | code_linesep = "\n" 32 | eol = os.linesep 33 | 34 | 35 | class Xml2Obj: 36 | """XML to Object""" 37 | def __init__(self): 38 | self.root = None 39 | self.nodeStack = [] 40 | self.attsToSkip = [] 41 | self._inCode = False 42 | self._mthdName = "" 43 | self._mthdCode = "" 44 | self._codeDict = None 45 | self._inProp = False 46 | self._propName = "" 47 | self._propData = "" 48 | self._propDict = None 49 | self._currPropAtt = "" 50 | self._currPropDict = None 51 | 52 | def StartElement(self, name, attributes): 53 | """SAX start element even handler""" 54 | if name == "code": 55 | # This is code for the parent element 56 | self._inCode = True 57 | parent = self.nodeStack[-1] 58 | if "code" not in parent: 59 | parent["code"] = {} 60 | self._codeDict = parent["code"] 61 | 62 | elif name == "properties": 63 | # These are the custom property definitions 64 | self._inProp = True 65 | self._propName = "" 66 | self._propData = "" 67 | parent = self.nodeStack[-1] 68 | if "properties" not in parent: 69 | parent["properties"] = {} 70 | self._propDict = parent["properties"] 71 | 72 | else: 73 | if self._inCode: 74 | self._mthdName = name.encode() 75 | elif self._inProp: 76 | if self._propName: 77 | # In the middle of a prop definition 78 | self._currPropAtt = name.encode() 79 | else: 80 | self._propName = name.encode() 81 | self._currPropDict = {} 82 | self._currPropAtt = "" 83 | else: 84 | element = {"name": name.encode()} 85 | if len(attributes) > 0: 86 | for att in self.attsToSkip: 87 | if att in attributes: 88 | del attributes[att] 89 | element["attributes"] = attributes 90 | 91 | # Push element onto the stack and make it a child of parent 92 | if len(self.nodeStack) > 0: 93 | parent = self.nodeStack[-1] 94 | if "children" not in parent: 95 | parent["children"] = [] 96 | parent["children"].append(element) 97 | else: 98 | self.root = element 99 | self.nodeStack.append(element) 100 | 101 | def EndElement(self, name): 102 | """SAX end element event handler""" 103 | if self._inCode: 104 | if name == "code": 105 | self._inCode = False 106 | self._codeDict = None 107 | else: 108 | # End of an individual method 109 | mth = self._mthdCode.strip() 110 | if not mth.endswith("\n"): 111 | mth += "\n" 112 | self._codeDict[self._mthdName] = mth 113 | self._mthdName = "" 114 | self._mthdCode = "" 115 | elif self._inProp: 116 | if name == "properties": 117 | self._inProp = False 118 | self._propDict = None 119 | elif name == self._propName: 120 | # End of an individual prop definition 121 | self._propDict[self._propName] = self._currPropDict 122 | self._propName = "" 123 | else: 124 | # end of a property attribute 125 | self._currPropDict[self._currPropAtt] = self._propData 126 | self._propData = self._currPropAtt = "" 127 | else: 128 | self.nodeStack = self.nodeStack[:-1] 129 | 130 | def CharacterData(self, data): 131 | """SAX character data event handler""" 132 | if self._inCode or data.strip(): 133 | data = data.replace("<", "<") 134 | data = data.encode() 135 | if self._inCode: 136 | if self._mthdCode: 137 | self._mthdCode += data 138 | else: 139 | self._mthdCode = data 140 | elif self._inProp: 141 | self._propData += data 142 | else: 143 | element = self.nodeStack[-1] 144 | if "cdata" not in element: 145 | element["cdata"] = "" 146 | element["cdata"] += data 147 | 148 | def Parse(self, xml): 149 | # Create a SAX parser 150 | Parser = expat.ParserCreate() 151 | # SAX event handlers 152 | Parser.StartElementHandler = self.StartElement 153 | Parser.EndElementHandler = self.EndElement 154 | Parser.CharacterDataHandler = self.CharacterData 155 | # Parse the XML File 156 | ParserStatus = Parser.Parse(xml, 1) 157 | return self.root 158 | 159 | def ParseFromFile(self, filename): 160 | return self.Parse(open(filename, "r").read()) 161 | 162 | 163 | def xmltodict(xml, attsToSkip=None, addCodeFile=False): 164 | """Given an xml string or file, return a Python dictionary.""" 165 | if not attsToSkip: 166 | attsToSkip = [] 167 | parser = Xml2Obj() 168 | parser.attsToSkip = attsToSkip 169 | isPath = os.path.exists(xml) 170 | errmsg = "" 171 | ret = None 172 | if eol not in xml and isPath: 173 | # argument was a file 174 | try: 175 | ret = parser.ParseFromFile(xml) 176 | except expat.ExpatError as e: 177 | errmsg = _("The XML in '%s' is not well-formed and cannot be parsed: %s") % (xml, e) 178 | else: 179 | # argument must have been raw xml: 180 | if not xml.strip().startswith(" 127: 226 | chars.append("&#%s;" % ord(char)) 227 | else: 228 | chars.append(char) 229 | val = "".join(chars) 230 | val = val.replace("<", "<").replace(">", ">") 231 | return "%s%s%s" % (qt, val, qt) 232 | 233 | 234 | def dicttoxml(dct, level=0, header=None, linesep=None): 235 | """Given a Python dictionary, return an xml string. 236 | 237 | The dictionary must be in the format returned by dicttoxml(), with keys 238 | on "attributes", "code", "cdata", "name", and "children". 239 | 240 | Send your own XML header, otherwise a default one will be used. 241 | 242 | The linesep argument is a dictionary, with keys on levels, allowing the 243 | developer to add extra whitespace depending on the level. 244 | """ 245 | att = "" 246 | ret = "" 247 | 248 | if "attributes" in dct: 249 | for key, val in list(dct["attributes"].items()): 250 | # Some keys are already handled. 251 | noEscape = key in ("sizerInfo",) 252 | val = escQuote(val, noEscape) 253 | att += " %s=%s" % (key, val) 254 | ret += "%s<%s%s" % ("\t" * level, dct["name"], att) 255 | 256 | if ("cdata" not in dct and "children" not in dct 257 | and "code" not in dct and "properties" not in dct): 258 | ret += " />%s" % eol 259 | else: 260 | ret += ">" 261 | if "cdata" in dct: 262 | ret += "%s" % dct["cdata"].replace("<", "<") 263 | 264 | if "code" in dct: 265 | if len(list(dct["code"].keys())): 266 | ret += "%s%s%s" % (eol, "\t" * (level + 1), eol) 267 | methodTab = "\t" * (level + 2) 268 | for mthd, cd in list(dct["code"].items()): 269 | # Convert \n's in the code to eol: 270 | cd = eol.join(cd.splitlines()) 271 | 272 | # Make sure that the code ends with a linefeed 273 | if not cd.endswith(eol): 274 | cd += eol 275 | 276 | ret += "%s<%s>%s%s%s" % ( 277 | methodTab, mthd, eol, 278 | cd, eol, 279 | methodTab, mthd, eol 280 | ) 281 | ret += "%s%s" % ("\t" * (level + 1), eol) 282 | 283 | if "properties" in dct: 284 | if len(list(dct["properties"].keys())): 285 | ret += "%s%s%s" % (eol, "\t" * (level + 1), eol) 286 | currTab = "\t" * (level + 2) 287 | for prop, val in list(dct["properties"].items()): 288 | ret += "%s<%s>%s" % (currTab, prop, eol) 289 | for propItm, itmVal in list(val.items()): 290 | itmTab = "\t" * (level + 3) 291 | ret += "%s<%s>%s%s" % (itmTab, propItm, itmVal, propItm, eol) 292 | ret += "%s%s" % (currTab, prop, eol) 293 | ret += "%s%s" % ("\t" * (level + 1), eol) 294 | 295 | if "children" in dct: 296 | if len(dct["children"]) > 0: 297 | ret += eol 298 | for child in dct["children"]: 299 | ret += dicttoxml(child, level + 1, linesep=linesep) 300 | indnt = "" 301 | if ret.endswith(eol): 302 | # Indent the closing tag 303 | indnt = ("\t" * level) 304 | ret += "%s%s" % (indnt, dct["name"], eol) 305 | 306 | if linesep: 307 | ret += linesep.get(level, "") 308 | 309 | if level == 0: 310 | if header is None: 311 | header = '%s' % (default_encoding, eol) 312 | ret = header + ret 313 | 314 | return ret 315 | 316 | 317 | def flattenClassDict(cd, retDict=None): 318 | """Given a dict containing a series of nested objects such as would 319 | be created by restoring from a cdxml file, returns a dict with all classIDs 320 | as keys, and a dict as the corresponding value. The dict value will have 321 | keys for the attributes and/or code, depending on what was in the original 322 | dict. The end result is to take a nested dict structure and return a flattened 323 | dict with all objects at the top level. 324 | """ 325 | if retDict is None: 326 | retDict = {} 327 | atts = cd.get("attributes", {}) 328 | props = cd.get("properties", {}) 329 | kids = cd.get("children", []) 330 | code = cd.get("code", {}) 331 | classID = atts.get("classID", "") 332 | classFile = resolvePath(atts.get("designerClass", "")) 333 | superclass = resolvePath(atts.get("superclass", "")) 334 | superclassID = atts.get("superclassID", "") 335 | if superclassID and os.path.exists(superclass): 336 | # Get the superclass info 337 | superCD = xmltodict(superclass, addCodeFile=True) 338 | flattenClassDict(superCD, retDict) 339 | if classID: 340 | if os.path.exists(classFile): 341 | # Get the class info 342 | classCD = xmltodict(classFile, addCodeFile=True) 343 | classAtts = classCD.get("attributes", {}) 344 | classProps = classCD.get("properties", {}) 345 | classCode = classCD.get("code", {}) 346 | classKids = classCD.get("children", []) 347 | currDict = retDict.get(classID, {}) 348 | retDict[classID] = { 349 | "attributes": classAtts, 350 | "code": classCode, 351 | "properties": classProps 352 | } 353 | retDict[classID].update(currDict) 354 | # Now update the child objects in the dict 355 | for kid in classKids: 356 | flattenClassDict(kid, retDict) 357 | else: 358 | # Not a file; most likely just a component in another class 359 | currDict = retDict.get(classID, {}) 360 | retDict[classID] = { 361 | "attributes": atts, 362 | "code": code, 363 | "properties": props 364 | } 365 | retDict[classID].update(currDict) 366 | if kids: 367 | for kid in kids: 368 | flattenClassDict(kid, retDict) 369 | return retDict 370 | 371 | 372 | def addInheritedInfo(src, super, updateCode=False): 373 | """Called recursively on the class container structure, modifying 374 | the attributes to incorporate superclass information. When the 375 | 'updateCode' parameter is True, superclass code is added to the 376 | object's code 377 | """ 378 | atts = src.get("attributes", {}) 379 | props = src.get("properties", {}) 380 | kids = src.get("children", []) 381 | code = src.get("code", {}) 382 | classID = atts.get("classID", "") 383 | if classID: 384 | superInfo = super.get(classID, {"attributes": {}, "code": {}, "properties": {}}) 385 | src["attributes"] = superInfo["attributes"].copy() 386 | src["attributes"].update(atts) 387 | src["properties"] = superInfo.get("properties", {}).copy() 388 | src["properties"].update(props) 389 | if updateCode: 390 | src["code"] = superInfo["code"].copy() 391 | src["code"].update(code) 392 | if kids: 393 | for kid in kids: 394 | addInheritedInfo(kid, super, updateCode) 395 | 396 | ''' 397 | # if __name__ == "__main__": 398 | # test_dict = {"name": "test", "attributes":{"path": "c:\\temp\\name", 399 | # "problemChars": "Welcome to \xc2\xae".decode("latin-1")}} 400 | # print "test_dict:", test_dict 401 | # xml = dicttoxml(test_dict) 402 | # print "xml:", xml 403 | # test_dict2 = xmltodict(xml) 404 | # print "test_dict2:", test_dict2 405 | # print "same?:", test_dict == test_dict2 406 | ''' 407 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------