From c706bf8e885d6ea933ae3d2f11ada01ddc5355b3 Mon Sep 17 00:00:00 2001 From: achbed Date: Wed, 19 Nov 2014 23:58:38 -0600 Subject: [PATCH] Core fix for inconsistent list object for station preferences 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 --- README.rst | 48 +++++++++++++++++ deefuzzer/core.py | 98 ++++++++++++++++++++++++++--------- deefuzzer/tools/utils.py | 72 +++++++++++++++++++++++++ example/deefuzzer.json | 11 ++++ example/deefuzzer.xml | 11 ++++ example/deefuzzer.yaml | 8 +++ example/deefuzzer_doc.xml | 49 +++++++++++++++++- example/stationconfig_doc.xml | 36 +++++++++++++ 8 files changed, 306 insertions(+), 27 deletions(-) create mode 100644 example/stationconfig_doc.xml diff --git a/README.rst b/README.rst index cea6454..e9715d4 100644 --- a/README.rst +++ b/README.rst @@ -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 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 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: + + + /path/to/media + + [name] + [name] + [name] + + + [path] + + + +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 === diff --git a/deefuzzer/core.py b/deefuzzer/core.py index ef0f1fc..84c9194 100644 --- a/deefuzzer/core.py +++ b/deefuzzer/core.py @@ -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: diff --git a/deefuzzer/tools/utils.py b/deefuzzer/tools/utils.py index df4955b..f8d4072 100644 --- a/deefuzzer/tools/utils.py +++ b/deefuzzer/tools/utils.py @@ -13,6 +13,11 @@ 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 diff --git a/example/deefuzzer.json b/example/deefuzzer.json index cc4078e..add4b6e 100644 --- a/example/deefuzzer.json +++ b/example/deefuzzer.json @@ -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, diff --git a/example/deefuzzer.xml b/example/deefuzzer.xml index 593d2ed..9cadbb1 100644 --- a/example/deefuzzer.xml +++ b/example/deefuzzer.xml @@ -1,6 +1,17 @@ /path/to/station.log /path/to/station.m3u + + + 0 + 16001 + + + /path/to/jingles + 0 + 1 + + 0 diff --git a/example/deefuzzer.yaml b/example/deefuzzer.yaml index 8e33454..366cb7f 100644 --- a/example/deefuzzer.yaml +++ b/example/deefuzzer.yaml @@ -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} diff --git a/example/deefuzzer_doc.xml b/example/deefuzzer_doc.xml index be45286..fd848dd 100644 --- a/example/deefuzzer_doc.xml +++ b/example/deefuzzer_doc.xml @@ -5,6 +5,28 @@ The file is preferably accessible behind an url, for example, http://mydomain.com/m3u/mystation.m3u --> /path/to/station.m3u + + + + + 0 + + 16001 + + + + /path/to/jingles + + 0 + + 1 + + - - + + + + /path/to/media + + + [name] + [name] + [name] + + + [path] + + + + + /path/to/configs + /path/to/configs2 + diff --git a/example/stationconfig_doc.xml b/example/stationconfig_doc.xml new file mode 100644 index 0000000..740b3c0 --- /dev/null +++ b/example/stationconfig_doc.xml @@ -0,0 +1,36 @@ + + + + My personal best funky playlist ever! + My best funky station + My_station + http://parisson.com + Various Funk Groove + + + /path/to/jingles + 0 + 1 + + + 96 + /path/to/mp3/ + mp3 + /path/to/m3u_file + 4 + 48000 + 0 + 2 + + + 127.0.0.1 + monitor + 8000 + 0 + icecast_source_password + icecast + 1 + + -- 2.39.5