]> git.parisson.com Git - deefuzzer.git/commitdiff
Core fix for inconsistent list object for station preferences
authorachbed <github@achbed.org>
Thu, 20 Nov 2014 05:58:38 +0000 (23:58 -0600)
committerachbed <github@achbed.org>
Thu, 20 Nov 2014 05:58:38 +0000 (23:58 -0600)
Added stationdefaults preference (apply default settings to all stations)
Added stationfolder preference (generate stations automagically from a folder structure)
Added stationconfig preference (load other preference files as stations)
Moved get_conf_dict to utils.py
Added documentation for the stationdefaults preference
Added documentation for the stationfolder and stationconfig preferences (deefuzzer_doc.xml only)

Signed-off-by: achbed <github@achbed.org>
README.rst
deefuzzer/core.py
deefuzzer/tools/utils.py
example/deefuzzer.json
example/deefuzzer.xml
example/deefuzzer.yaml
example/deefuzzer_doc.xml
example/stationconfig_doc.xml [new file with mode: 0644]

index cea6454a15aeffe1b709b9307afabdf74874e2a5..e9715d4087760d2004509378259ff465f8748c0e 100644 (file)
@@ -176,6 +176,7 @@ Then any OSC remote (PureDate, Monome, TouchOSC, etc..) can a become controller!
 We provide some client python scripts as some examples about how to control the parameters
 from a console or any application (see deefuzzer/scripts/).
 
+
 Twitter (manual and optional)
 ================================
 
@@ -207,6 +208,53 @@ For example::
 
 Your DeeFuzzer will now tweet the currently playing track and new tracks on your profile.
 
+
+Station Folders
+===============
+
+Station folders are a specific way of setting up your file system so that you can auto-create many stations
+based on only a few settings.  The feature requires a single main folder, with one or more subfolders.  Each
+subfolder is scanned for the presence of media files (audio-only at the moment).  If files are found, then a
+station is created using the parameters in the <stationfolder> block.  Substitution is performed to fill in
+some detail to the stationfolder parameters, and all stationdefaults are also applied.
+
+The base folder is specified by the <folder> block.  No substitution is done on this parameter.
+
+Subsitution is done for [name] and [path] - [name] is replaced with the name of the subfolder, and [path] is
+replaced with the subfolder's complete path.
+
+Consider the following example.  We have a block with the following settings:
+
+               <stationfolder>
+                               <folder>/path/to/media</folder>
+                               <infos>
+                                               <short_name>[name]</short_name>
+                                               <name>[name]</name>
+                                               <genre>[name]</genre>
+                               </infos>
+                               <media>
+                                               <dir>[path]</dir>
+                               </media>
+               </stationfolder>
+
+The folder structure is as follows:
+
+               /path/to/media
+                               + one
+                                               - song1.mp3
+                                               - song2.mp3
+                               + two
+                                               - song3.ogg
+                               + three
+                                               - presentation.pdf
+                               + four
+                                               - song4.mp3
+
+In this case, three stations are created:  one, two, and four.  Each will have their short name (and thus their
+icecast mount point) set to their respective folder names.  Subfolder three is skipped, as there are no audio files
+present - just a PDF file.
+
+
 API
 ===
 
index ef0f1fc65adb5b979f1ace20727cd0c82af35e94..84c919473f0c7f1b7ed1b5b316b58d445d916237 100644 (file)
@@ -57,8 +57,7 @@ class DeeFuzzer(Thread):
     def __init__(self, conf_file):
         Thread.__init__(self)
         self.conf_file = conf_file
-        self.conf = self.get_conf_dict()
-
+        self.conf = get_conf_dict(self.conf_file)
         for key in self.conf['deefuzzer'].keys():
             if key == 'log':
                 log_file = self.conf['deefuzzer']['log']
@@ -71,11 +70,24 @@ class DeeFuzzer(Thread):
             else:
                 setattr(self, key, self.conf['deefuzzer'][key])
 
-        if isinstance(self.conf['deefuzzer']['station'], dict):
-            # Fix wrong type data from xmltodict when one station (*)
-            self.nb_stations = 1
+        # Fix wrong type data from xmltodict when one station (*)
+        if 'station' not in self.conf['deefuzzer']:
+            self.conf['deefuzzer']['station'] = []
         else:
-            self.nb_stations = len(self.conf['deefuzzer']['station'])
+            if not isinstance(self.conf['deefuzzer']['station'], list):
+                s = self.conf['deefuzzer']['station']
+                self.conf['deefuzzer']['station'] = []
+                self.conf['deefuzzer']['station'].append(s)
+        
+        # Load additional station definitions from the requested folder
+        if 'stationconfig' in self.conf['deefuzzer']:
+            self.load_stations(self.conf['deefuzzer']['stationconfig'])
+        
+        # Create stations automagically from a folder structure
+        if 'stationfolder' in self.conf['deefuzzer'].keys() and isinstance(self.conf['deefuzzer']['stationfolder'], dict):
+            self.create_stations_fromfolder(self.conf['deefuzzer']['stationfolder'])
+
+        self.nb_stations = len(self.conf['deefuzzer']['station'])
 
         # Set the deefuzzer logger
         self.logger.write_info('Starting DeeFuzzer')
@@ -85,21 +97,6 @@ class DeeFuzzer(Thread):
         self.stations = []
         self.logger.write_info('Number of stations : ' + str(self.nb_stations))
 
-    def get_conf_dict(self):
-        mime_type = mimetypes.guess_type(self.conf_file)[0]
-        confile = open(self.conf_file,'r')
-        data = confile.read()
-        confile.close()
-
-        if 'xml' in mime_type:
-            return xmltodict(data,'utf-8')
-        elif 'yaml' in mime_type:
-            import yaml
-            return yaml.load(data)
-        elif 'json' in mime_type:
-            import json
-            return json.loads(data)
-
     def set_m3u_playlist(self):
         m3u_dir = os.sep.join(self.m3u.split(os.sep)[:-1])
         if not os.path.exists(m3u_dir) and m3u_dir:
@@ -112,14 +109,65 @@ class DeeFuzzer(Thread):
         m3u.close()
         self.logger.write_info('Writing M3U file to : ' + self.m3u)
 
+    def create_stations_fromfolder(self, options):
+        if not 'folder' in options.keys():
+            return
+        folder = str(options['folder'])
+        self.logger.write_info('Scanning folder ' + folder + ' for stations...')
+        files = os.listdir(folder)
+        for file in files:
+            filepath = os.path.join(folder, file)
+            if os.path.isdir(filepath):
+                if folder_contains_music(filepath):
+                    self.create_station(filepath, options)
+
+    def create_station(self, folder, options):
+        self.logger.write_info('Creating station for folder ' + folder)
+        s = {}
+        path, name = os.path.split(folder)
+        d = dict(path=folder,name=name)
+        for i in options.keys():
+            if not 'folder' in i:
+                s[i] = replace_all(options[i], d)
+        if not 'media' in s.keys():
+            s['media'] = {}
+        s['media']['dir'] = folder:
+        self.conf['deefuzzer']['station'].append(s)
+    
+        
+    def load_stations(self, folder):
+        if isinstance(folder, dict):
+            for f in folder:
+                self.load_stations(f)
+            return
+
+        if not os.path.isdir(folder):
+            return
+        
+        self.logger.write_info('Loading station config files in ' + folder)
+        files = os.listdir(folder)
+        for file in files:
+            filepath = os.path.join(folder, file)
+            if os.path.isfile(filepath):
+                self.load_station_config(filepath)
+        
+    def load_station_config(self, file):
+        self.logger.write_info('Loading station config file ' + file)
+        stationdef = get_conf_dict(file)
+        if isinstance(stationdef, dict):
+            if 'station' in stationdef.keys() and isinstance(stationdef['station'], dict):
+                self.conf['deefuzzer']['station'].append(stationdef['station'])
+
     def run(self):
         q = Queue.Queue(1)
 
         for i in range(0,self.nb_stations):
-            if isinstance(self.conf['deefuzzer']['station'], dict):
-                station = self.conf['deefuzzer']['station']
-            else:
-                station = self.conf['deefuzzer']['station'][i]
+            station = self.conf['deefuzzer']['station'][i]
+
+            # Apply station defaults if they exist
+            if 'stationdefaults' in self.conf['deefuzzer']:
+                if isinstance(self.conf['deefuzzer']['stationdefaults'], dict):
+                    station = merge_defaults(station, self.conf['deefuzzer']['stationdefaults'])
             self.stations.append(Station(station, q, self.logger, self.m3u))
 
         if self.m3u:
index df4955b879399b8a9b377059cdc1c02a5c91864e..f8d4072d1c1e89c4de4ae1c504060123353c67f7 100644 (file)
 import os
 import re
 import string
+import mimetypes
+from itertools import chain
+from deefuzzer.tools import *
+
+mimetypes.add_type('application/x-yaml','.yaml')
 
 def clean_word(word) :
     """ Return the word without excessive blank spaces, underscores and
@@ -37,3 +42,70 @@ def get_file_info(media):
 
 def is_absolute_path(path):
     return os.sep == path[0]
+
+def merge_defaults(setting, default):
+    combined = {}
+    for key in set(chain(setting, default)):
+        if key in setting:
+            if key in default:
+                if isinstance(setting[key], dict) and isinstance(default[key], dict):
+                    combined[key] = merge_defaults(setting[key], default[key])
+                else:
+                    combined[key] = setting[key]
+            else:
+                combined[key] = setting[key]
+        else:
+            combined[key] = default[key]
+    return combined
+
+def replace_all(option, repl):
+    if isinstance(option, list):
+        r = []
+        for i in option:
+            r.append(replace_all(i, repl))
+        return r
+    elif isinstance(option, dict):
+        r = {}
+        for key in option.keys():
+            r[key] = replace_all(option[key], repl)
+        return r
+    elif isinstance(option, str):
+        r = option
+        for key in repl.keys():
+          r = r.replace('[' + key + ']', repl[key])
+        return r
+    return option
+
+def get_conf_dict(file):
+    mime_type = mimetypes.guess_type(file)[0]
+
+    # Do the type check first, so we don't load huge files that won't be used
+    if 'xml' in mime_type:
+        confile = open(file,'r')
+        data = confile.read()
+        confile.close()
+        return xmltodict(data,'utf-8')
+    elif 'yaml' in mime_type:
+        import yaml
+        confile = open(file,'r')
+        data = confile.read()
+        confile.close()
+        return yaml.load(data)
+    elif 'json' in mime_type:
+        import json
+        confile = open(file,'r')
+        data = confile.read()
+        confile.close()
+        return json.loads(data)
+    
+    return False
+
+def folder_contains_music(folder):
+    files = os.listdir(folder)
+    for file in files:
+        filepath = os.path.join(folder, file)
+        if os.path.isfile(filepath):
+            mime_type = mimetypes.guess_type(filepath)[0]
+            if 'audio/mpeg' in mime_type or 'audio/ogg' in mime_type:
+                return True
+    return False
index cc4078e36b8951d87ad36a4fe112e0b73f36be9d..add4b6e1901d964efb4e24e57c7b47b3e34c8d4f 100644 (file)
@@ -2,6 +2,17 @@
     "deefuzzer": {
         "log": "/path/to/station.log",
         "m3u": "/path/to/station.m3u",
+        "stationdefaults": {
+            "control": {
+                "mode": 0,
+                "port": 16001
+            },
+            "jingles": {
+                "dir": "/path/to/jingles",
+                "mode": 0,
+                "shuffle": 1
+            }
+        },
         "station": {
             "control": {
                 "mode": 0,
index 593d2edce0973e8d0c1ec20d208dddfdadfbd5f9..9cadbb14d8e45684607f63c9ffa9a433e54e1182 100644 (file)
@@ -1,6 +1,17 @@
 <deefuzzer>
     <log>/path/to/station.log</log>
     <m3u>/path/to/station.m3u</m3u>
+    <stationdefaults>
+        <control>
+            <mode>0</mode>
+            <port>16001</port>
+        </control>
+        <jingles>
+            <dir>/path/to/jingles</dir>
+            <mode>0</mode>
+            <shuffle>1</shuffle>
+        </jingles>
+    </stationdefaults>
     <station>
         <control>
             <mode>0</mode>
index 8e33454cd3deee8477c1ea5ef902310c6969b8aa..366cb7f018da5ff39fba7c2c03425374d4d8ed42 100644 (file)
@@ -2,6 +2,14 @@ deefuzzer:
   log: /path/to/station.log
   m3u: /path/to/station.m3u
 
+  stationdefaults:
+    control: {mode: 0,
+             port: 16001}
+
+    jingles: {dir: /path/to/jingles,
+              mode: 0,
+              shuffle: 1}
+
   station:
     control: {mode: 0,
              port: 16001}
index be45286990dfb1b7003e38c22c0a90fb8e335984..fd848dd2a2e05e1cd6b04c38608451fece628372 100644 (file)
@@ -5,6 +5,28 @@
          The file is preferably accessible behind an url,
          for example, http://mydomain.com/m3u/mystation.m3u -->
     <m3u>/path/to/station.m3u</m3u>
+    <stationdefaults>
+      <!-- This tag allows a common default configuration to be set for all stations.  This
+           is useful when defining many stations that will share many common configuration
+           settings.  If a setting is specified here and in a station tag, the station tag 
+           will override this one.  Available options are the same as the station tag. -->
+        <control>
+            <!-- If '1', an OSC controller thread is started to allow external commands
+                See README for more info -->
+            <mode>0</mode>
+            <!-- The port of the OSC server -->
+            <port>16001</port>
+        </control>
+        <jingles>
+            <!-- A path to the directory containing jingles media files.
+                The files have to be of the same type of the main media files. -->
+            <dir>/path/to/jingles</dir>
+            <!-- If '1', some media will be played between each main track of the playlist. '0' does nothing. -->
+            <mode>0</mode>
+            <!-- If '1', the jingle playlist will be randomized. '0' for aphanumeric order -->
+            <shuffle>1</shuffle>
+        </jingles>
+    </stationdefaults>
     <station>
         <control>
             <!-- If '1', an OSC controller thread is started to allow external commands
     </station>
 
     <!-- Note that you can add many different stations in the same config file, thanks to the multi-threaded architecure ! -->
-</deefuzzer>
-
 
+    <!-- The stationfolder option allows auto-creation of stations based on a folder structure.  See the readme
+         for details. -->
+    <stationfolder>
+        <!-- REQUIRED: The base folder to use when auto-generating stations -->
+        <folder>/path/to/media</folder>
+        <!-- Station information to use.  At a minimum, the following should be defined:
+                infos.short_name so that mount points will be unique. 
+                media.dir so that the files are loaded from the right place (IMPROVEMENT: should be set in code!)
+             All the same options are available as the station setting, and all stations will also have the global
+             stationdefaults applied.  -->
+        <infos>
+            <short_name>[name]</short_name>
+            <name>[name]</name>
+            <genre>[name]</genre>
+        </infos>
+        <media>
+            <dir>[path]</dir>
+        </media>
+    </stationfolder>
+               
+    <!-- The stationfolder option allows specifying a folder to scan for additional configuration files.  Applies only 
+                                those that are station blocks.  Can specify multiple stationoption blocks.  -->
+               <stationconfig>/path/to/configs</stationconfig>
+               <stationconfig>/path/to/configs2</stationconfig>
+</deefuzzer>
diff --git a/example/stationconfig_doc.xml b/example/stationconfig_doc.xml
new file mode 100644 (file)
index 0000000..740b3c0
--- /dev/null
@@ -0,0 +1,36 @@
+<!-- This is an example of a station definition file loaded via the stationconfig option.  One or more station 
+     definitions can be defied here.  No other blocks will be loaded if they are present.  All stations will 
+     have the global stationdefaults applied.  -->
+<station>
+    <infos>
+        <description>My personal best funky playlist ever!</description>
+        <name>My best funky station</name>
+        <short_name>My_station</short_name>
+        <url>http://parisson.com</url>
+        <genre>Various Funk Groove</genre>
+    </infos>
+    <jingles>
+        <dir>/path/to/jingles</dir>
+        <mode>0</mode>
+        <shuffle>1</shuffle>
+    </jingles>
+    <media>
+        <bitrate>96</bitrate>
+        <dir>/path/to/mp3/</dir>
+        <format>mp3</format>
+        <m3u>/path/to/m3u_file</m3u>
+        <ogg_quality>4</ogg_quality>
+        <samplerate>48000</samplerate>
+        <shuffle>0</shuffle>
+        <voices>2</voices>
+    </media>
+    <server>
+        <host>127.0.0.1</host>
+        <mountpoint>monitor</mountpoint>
+        <port>8000</port>
+        <public>0</public>
+        <sourcepassword>icecast_source_password</sourcepassword>
+        <type>icecast</type>
+        <appendtype>1</appendtype>
+    </server>
+</station>