diff --git a/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py b/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py index 39ab70400..2dd81c677 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/analyzer_pipeline.py @@ -6,6 +6,9 @@ import multiprocessing from metadata_analyzer import MetadataAnalyzer from filemover_analyzer import FileMoverAnalyzer from cloud_storage_uploader import CloudStorageUploader +from cuepoint_analyzer import CuePointAnalyzer +from replaygain_analyzer import ReplayGainAnalyzer +from playability_analyzer import * class AnalyzerPipeline: """ Analyzes and imports an audio file into the Airtime library. @@ -54,10 +57,14 @@ class AnalyzerPipeline: metadata = dict() metadata = MetadataAnalyzer.analyze(audio_file_path, metadata) metadata["station_domain"] = station_domain - #metadata = FileMoverAnalyzer.move(audio_file_path, import_directory, original_filename, metadata) + + metadata = CuePointAnalyzer.analyze(audio_file_path, metadata) + metadata = ReplayGainAnalyzer.analyze(audio_file_path, metadata) + metadata = PlayabilityAnalyzer.analyze(audio_file_path, metadata) + csu = CloudStorageUploader() metadata = csu.upload_obj(audio_file_path, metadata) - metadata["import_status"] = 0 # imported + metadata["import_status"] = 0 # Successfully imported # Note that the queue we're putting the results into is our interprocess communication # back to the main process. @@ -65,6 +72,11 @@ class AnalyzerPipeline: # Pass all the file metadata back to the main analyzer process, which then passes # it back to the Airtime web application. queue.put(metadata) + except UnplayableFileError as e: + logging.exception(e) + metadata["import_status"] = 2 + metadata["reason"] = "The file could not be played." + raise e except Exception as e: # Ensures the traceback for this child process gets written to our log files: logging.exception(e) diff --git a/python_apps/airtime_analyzer/airtime_analyzer/cuepoint_analyzer.py b/python_apps/airtime_analyzer/airtime_analyzer/cuepoint_analyzer.py new file mode 100644 index 000000000..b5492ebe9 --- /dev/null +++ b/python_apps/airtime_analyzer/airtime_analyzer/cuepoint_analyzer.py @@ -0,0 +1,45 @@ +import subprocess +import logging +import traceback +import json +import datetime +from analyzer import Analyzer + + +class CuePointAnalyzer(Analyzer): + ''' This class extracts the cue-in time, cue-out time, and length of a track using silan. ''' + + SILAN_EXECUTABLE = 'silan' + + @staticmethod + def analyze(filename, metadata): + ''' Extracts the cue-in and cue-out times along and sets the file duration based on that. + The cue points are there to skip the silence at the start and end of a track, and are determined + using "silan", which analyzes the loudness in a track. + :param filename: The full path to the file to analyzer + :param metadata: A metadata dictionary where the results will be put + :return: The metadata dictionary + ''' + ''' The silan -F 0.99 parameter tweaks the highpass filter. The default is 0.98, but at that setting, + the unit test on the short m4a file fails. With the new setting, it gets the correct cue-in time and + all the unit tests pass. + ''' + command = [CuePointAnalyzer.SILAN_EXECUTABLE, '-b', '-F', '0.99', '-f', 'JSON', filename] + try: + results_json = subprocess.check_output(command, stderr=subprocess.STDOUT) + silan_results = json.loads(results_json) + metadata['length_seconds'] = float(silan_results['file duration']) + # Conver the length into a formatted time string + track_length = datetime.timedelta(seconds=metadata['length_seconds']) + metadata["length"] = str(track_length) + metadata['cuein'] = silan_results['sound'][0][0] + metadata['cueout'] = silan_results['sound'][0][1] + + except OSError as e: # silan was not found + logging.warn("Failed to run: %s - %s. %s" % (command[0], e.strerror, "Do you have silan installed?")) + except subprocess.CalledProcessError as e: # silan returned an error code + logging.warn("%s %s %s", e.cmd, e.message, e.returncode) + except Exception as e: + logging.warn(e) + + return metadata \ No newline at end of file diff --git a/python_apps/airtime_analyzer/airtime_analyzer/metadata_analyzer.py b/python_apps/airtime_analyzer/airtime_analyzer/metadata_analyzer.py index a7bd0a304..45645bd32 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/metadata_analyzer.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/metadata_analyzer.py @@ -149,7 +149,7 @@ class MetadataAnalyzer(Analyzer): metadata["mime"] = magic.from_file(filename, mime=True) metadata["channels"] = reader.getnchannels() metadata["sample_rate"] = reader.getframerate() - length_seconds = float(reader.getnframes()) / float(metadata["channels"] * metadata["sample_rate"]) + length_seconds = float(reader.getnframes()) / float(metadata["sample_rate"]) #Converting the length in seconds (float) to a formatted time string track_length = datetime.timedelta(seconds=length_seconds) metadata["length"] = str(track_length) #time.strftime("%H:%M:%S.%f", track_length) diff --git a/python_apps/airtime_analyzer/airtime_analyzer/playability_analyzer.py b/python_apps/airtime_analyzer/airtime_analyzer/playability_analyzer.py new file mode 100644 index 000000000..0a3656296 --- /dev/null +++ b/python_apps/airtime_analyzer/airtime_analyzer/playability_analyzer.py @@ -0,0 +1,32 @@ +__author__ = 'asantoni' + +import subprocess +import logging +from analyzer import Analyzer + +class UnplayableFileError(Exception): + pass + +class PlayabilityAnalyzer(Analyzer): + ''' This class checks if a file can actually be played with Liquidsoap. ''' + + LIQUIDSOAP_EXECUTABLE = 'liquidsoap' + + @staticmethod + def analyze(filename, metadata): + ''' Checks if a file can be played by Liquidsoap. + :param filename: The full path to the file to analyzer + :param metadata: A metadata dictionary where the results will be put + :return: The metadata dictionary + ''' + command = [PlayabilityAnalyzer.LIQUIDSOAP_EXECUTABLE, '-v', '-c', "output.dummy(audio_to_stereo(single('%s')))" % filename] + try: + subprocess.check_output(command, stderr=subprocess.STDOUT) + + except OSError as e: # liquidsoap was not found + logging.warn("Failed to run: %s - %s. %s" % (command[0], e.strerror, "Do you have liquidsoap installed?")) + except (subprocess.CalledProcessError, Exception) as e: # liquidsoap returned an error code + logging.warn(e) + raise UnplayableFileError + + return metadata diff --git a/python_apps/airtime_analyzer/airtime_analyzer/replaygain_analyzer.py b/python_apps/airtime_analyzer/airtime_analyzer/replaygain_analyzer.py index 2d02518f2..396d0d5ad 100644 --- a/python_apps/airtime_analyzer/airtime_analyzer/replaygain_analyzer.py +++ b/python_apps/airtime_analyzer/airtime_analyzer/replaygain_analyzer.py @@ -1,14 +1,36 @@ +import subprocess +import logging from analyzer import Analyzer -''' TODO: ReplayGain is currently calculated by pypo but it should - be done here in the analyzer. -''' + class ReplayGainAnalyzer(Analyzer): - - def __init__(self): - pass - + ''' This class extracts the ReplayGain using a tool from the python-rgain package. ''' + + REPLAYGAIN_EXECUTABLE = 'replaygain' # From the python-rgain package + @staticmethod - def analyze(filename): - pass - + def analyze(filename, metadata): + ''' Extracts the Replaygain loudness normalization factor of a track. + :param filename: The full path to the file to analyzer + :param metadata: A metadata dictionary where the results will be put + :return: The metadata dictionary + ''' + ''' The -d flag means do a dry-run, ie. don't modify the file directly. + ''' + command = [ReplayGainAnalyzer.REPLAYGAIN_EXECUTABLE, '-d', filename] + try: + results = subprocess.check_output(command, stderr=subprocess.STDOUT) + filename_token = "%s: " % filename + rg_pos = results.find(filename_token, results.find("Calculating Replay Gain information")) + len(filename_token) + db_pos = results.find(" dB", rg_pos) + replaygain = results[rg_pos:db_pos] + metadata['replay_gain'] = float(replaygain) + + except OSError as e: # replaygain was not found + logging.warn("Failed to run: %s - %s. %s" % (command[0], e.strerror, "Do you have python-rgain installed?")) + except subprocess.CalledProcessError as e: # replaygain returned an error code + logging.warn("%s %s %s", e.cmd, e.message, e.returncode) + except Exception as e: + logging.warn(e) + + return metadata \ No newline at end of file diff --git a/python_apps/airtime_analyzer/setup.py b/python_apps/airtime_analyzer/setup.py index 61bbd55b2..f576d0409 100644 --- a/python_apps/airtime_analyzer/setup.py +++ b/python_apps/airtime_analyzer/setup.py @@ -28,8 +28,9 @@ setup(name='airtime_analyzer', 'coverage', 'mock', 'python-daemon', - 'requests', + 'requests' 'apache-libcloud', + 'rgain', # These next 3 are required for requests to support SSL with SNI. Learned this the hard way... # What sucks is that GCC is required to pip install these. #'ndg-httpsclient', diff --git a/python_apps/airtime_analyzer/tests/cuepoint_analyzer_tests.py b/python_apps/airtime_analyzer/tests/cuepoint_analyzer_tests.py new file mode 100644 index 000000000..a55b6ee98 --- /dev/null +++ b/python_apps/airtime_analyzer/tests/cuepoint_analyzer_tests.py @@ -0,0 +1,63 @@ +from nose.tools import * +from airtime_analyzer.cuepoint_analyzer import CuePointAnalyzer + +def check_default_metadata(metadata): + ''' Check that the values extract by Silan/CuePointAnalyzer on our test audio files match what we expect. + :param metadata: a metadata dictionary + :return: Nothing + ''' + # We give silan some leeway here by specifying a tolerance + tolerance_seconds = 0.1 + length_seconds = 3.9 + assert abs(metadata['length_seconds'] - length_seconds) < tolerance_seconds + assert abs(metadata['cuein']) < tolerance_seconds + assert abs(metadata['cueout'] - length_seconds) < tolerance_seconds + +def test_missing_silan(): + old_silan = CuePointAnalyzer.SILAN_EXECUTABLE + CuePointAnalyzer.SILAN_EXECUTABLE = 'foosdaf' + metadata = CuePointAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo-utf8.mp3', dict()) + CuePointAnalyzer.SILAN_EXECUTABLE = old_silan # Need to put this back + +def test_invalid_filepath(): + metadata = CuePointAnalyzer.analyze(u'non-existent-file', dict()) + + +def test_mp3_utf8(): + metadata = CuePointAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo-utf8.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_dualmono(): + metadata = CuePointAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-dualmono.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_jointstereo(): + metadata = CuePointAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-jointstereo.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_simplestereo(): + metadata = CuePointAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-simplestereo.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_stereo(): + metadata = CuePointAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_mono(): + metadata = CuePointAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-mono.mp3', dict()) + check_default_metadata(metadata) + +def test_ogg_stereo(): + metadata = CuePointAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.ogg', dict()) + check_default_metadata(metadata) + +def test_invalid_wma(): + metadata = CuePointAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo-invalid.wma', dict()) + +def test_m4a_stereo(): + metadata = CuePointAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.m4a', dict()) + check_default_metadata(metadata) + +def test_wav_stereo(): + metadata = CuePointAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.wav', dict()) + check_default_metadata(metadata) diff --git a/python_apps/airtime_analyzer/tests/metadata_analyzer_tests.py b/python_apps/airtime_analyzer/tests/metadata_analyzer_tests.py index 9215c493a..ee108b362 100644 --- a/python_apps/airtime_analyzer/tests/metadata_analyzer_tests.py +++ b/python_apps/airtime_analyzer/tests/metadata_analyzer_tests.py @@ -114,6 +114,18 @@ def test_mp3_utf8(): assert metadata['mime'] == 'audio/mp3' assert metadata['track_total'] == u'10' # MP3s can have a track_total +def test_invalid_wma(): + metadata = MetadataAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo-invalid.wma', dict()) + assert metadata['mime'] == 'audio/x-ms-wma' + +def test_wav_stereo(): + metadata = MetadataAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.wav', dict()) + assert metadata['mime'] == 'audio/x-wav' + assert abs(metadata['length_seconds'] - 3.9) < 0.1 + assert metadata['channels'] == 2 + assert metadata['sample_rate'] == 44100 + + # Make sure the parameter checking works @raises(TypeError) def test_move_wrong_string_param1(): @@ -132,7 +144,6 @@ def test_mp3_bad_channels(): It'd be a pain in the ass to construct a real MP3 with an invalid number of channels by hand because that value is stored in every MP3 frame in the file ''' - print "testing bad channels..." audio_file = mutagen.File(filename, easy=True) audio_file.info.mode = 1777 with mock.patch('airtime_analyzer.metadata_analyzer.mutagen') as mock_mutagen: @@ -143,7 +154,6 @@ def test_mp3_bad_channels(): check_default_metadata(metadata) assert metadata['channels'] == 1 assert metadata['bit_rate'] == 64000 - print metadata['length_seconds'] assert abs(metadata['length_seconds'] - 3.9) < 0.1 assert metadata['mime'] == 'audio/mp3' # Not unicode because MIMEs aren't. assert metadata['track_total'] == u'10' # MP3s can have a track_total diff --git a/python_apps/airtime_analyzer/tests/playability_analyzer_tests.py b/python_apps/airtime_analyzer/tests/playability_analyzer_tests.py new file mode 100644 index 000000000..3864d6b40 --- /dev/null +++ b/python_apps/airtime_analyzer/tests/playability_analyzer_tests.py @@ -0,0 +1,61 @@ +from nose.tools import * +from airtime_analyzer.playability_analyzer import * + +def check_default_metadata(metadata): + ''' Stub function for now in case we need it later.''' + pass + +def test_missing_liquidsoap(): + old_ls = PlayabilityAnalyzer.LIQUIDSOAP_EXECUTABLE + PlayabilityAnalyzer.LIQUIDSOAP_EXECUTABLE = 'foosdaf' + metadata = PlayabilityAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo-utf8.mp3', dict()) + PlayabilityAnalyzer.LIQUIDSOAP_EXECUTABLE = old_ls # Need to put this back + +@raises(UnplayableFileError) +def test_invalid_filepath(): + metadata = PlayabilityAnalyzer.analyze(u'non-existent-file', dict()) + +def test_mp3_utf8(): + metadata = PlayabilityAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo-utf8.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_dualmono(): + metadata = PlayabilityAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-dualmono.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_jointstereo(): + metadata = PlayabilityAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-jointstereo.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_simplestereo(): + metadata = PlayabilityAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-simplestereo.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_stereo(): + metadata = PlayabilityAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_mono(): + metadata = PlayabilityAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-mono.mp3', dict()) + check_default_metadata(metadata) + +def test_ogg_stereo(): + metadata = PlayabilityAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.ogg', dict()) + check_default_metadata(metadata) + +@raises(UnplayableFileError) +def test_invalid_wma(): + metadata = PlayabilityAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo-invalid.wma', dict()) + +def test_m4a_stereo(): + metadata = PlayabilityAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.m4a', dict()) + check_default_metadata(metadata) + +def test_wav_stereo(): + metadata = PlayabilityAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.wav', dict()) + check_default_metadata(metadata) + +@raises(UnplayableFileError) +def test_unknown(): + metadata = PlayabilityAnalyzer.analyze(u'http://www.google.com', dict()) + check_default_metadata(metadata) \ No newline at end of file diff --git a/python_apps/airtime_analyzer/tests/replaygain_analyzer_tests.py b/python_apps/airtime_analyzer/tests/replaygain_analyzer_tests.py new file mode 100644 index 000000000..c9e98bfb3 --- /dev/null +++ b/python_apps/airtime_analyzer/tests/replaygain_analyzer_tests.py @@ -0,0 +1,71 @@ +from nose.tools import * +from airtime_analyzer.replaygain_analyzer import ReplayGainAnalyzer + +def check_default_metadata(metadata): + ''' Check that the values extract by Silan/CuePointAnalyzer on our test audio files match what we expect. + :param metadata: a metadata dictionary + :return: Nothing + ''' + ''' + # We give python-rgain some leeway here by specifying a tolerance. It's not perfectly consistent across codecs... + assert abs(metadata['cuein']) < tolerance_seconds + assert abs(metadata['cueout'] - length_seconds) < tolerance_seconds + ''' + tolerance = 0.30 + expected_replaygain = 5.0 + print metadata['replay_gain'] + assert abs(metadata['replay_gain'] - expected_replaygain) < tolerance + +def test_missing_replaygain(): + old_rg = ReplayGainAnalyzer.REPLAYGAIN_EXECUTABLE + ReplayGainAnalyzer.REPLAYGAIN_EXECUTABLE = 'foosdaf' + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo-utf8.mp3', dict()) + ReplayGainAnalyzer.REPLAYGAIN_EXECUTABLE = old_rg # Need to put this back + +def test_invalid_filepath(): + metadata = ReplayGainAnalyzer.analyze(u'non-existent-file', dict()) + + +def test_mp3_utf8(): + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo-utf8.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_dualmono(): + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-dualmono.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_jointstereo(): + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-jointstereo.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_simplestereo(): + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-simplestereo.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_stereo(): + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.mp3', dict()) + check_default_metadata(metadata) + +def test_mp3_mono(): + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-mono.mp3', dict()) + check_default_metadata(metadata) + +def test_ogg_stereo(): + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.ogg', dict()) + check_default_metadata(metadata) + +def test_invalid_wma(): + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo-invalid.wma', dict()) + +def test_mp3_missing_id3_header(): + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-mp3-missingid3header.mp3', dict()) + +def test_m4a_stereo(): + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.m4a', dict()) + check_default_metadata(metadata) + +''' WAVE is not supported by python-rgain yet +def test_wav_stereo(): + metadata = ReplayGainAnalyzer.analyze(u'tests/test_data/44100Hz-16bit-stereo.wav', dict()) + check_default_metadata(metadata) +''' \ No newline at end of file diff --git a/python_apps/airtime_analyzer/tests/test_data/44100Hz-16bit-mp3-missingid3header.mp3 b/python_apps/airtime_analyzer/tests/test_data/44100Hz-16bit-mp3-missingid3header.mp3 new file mode 100644 index 000000000..0e7181f64 Binary files /dev/null and b/python_apps/airtime_analyzer/tests/test_data/44100Hz-16bit-mp3-missingid3header.mp3 differ diff --git a/python_apps/airtime_analyzer/tests/test_data/44100Hz-16bit-stereo-invalid.wma b/python_apps/airtime_analyzer/tests/test_data/44100Hz-16bit-stereo-invalid.wma new file mode 100644 index 000000000..f0bf3f19c Binary files /dev/null and b/python_apps/airtime_analyzer/tests/test_data/44100Hz-16bit-stereo-invalid.wma differ diff --git a/python_apps/airtime_analyzer/tests/test_data/44100Hz-16bit-stereo.wav b/python_apps/airtime_analyzer/tests/test_data/44100Hz-16bit-stereo.wav new file mode 100644 index 000000000..88b733078 Binary files /dev/null and b/python_apps/airtime_analyzer/tests/test_data/44100Hz-16bit-stereo.wav differ diff --git a/python_apps/pypo/media/__init__.py b/python_apps/pypo/media/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_apps/pypo/media/update/__init__.py b/python_apps/pypo/media/update/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python_apps/pypo/media/update/replaygain.py b/python_apps/pypo/media/update/replaygain.py deleted file mode 100644 index 431933bda..000000000 --- a/python_apps/pypo/media/update/replaygain.py +++ /dev/null @@ -1,161 +0,0 @@ -from subprocess import Popen, PIPE -import re -import os -import sys -import shutil -import tempfile -import logging - - -logger = logging.getLogger() - -def get_process_output(command): - """ - Run subprocess and return stdout - """ - logger.debug(command) - p = Popen(command, stdout=PIPE, stderr=PIPE) - return p.communicate()[0].strip() - -def run_process(command): - """ - Run subprocess and return "return code" - """ - p = Popen(command, stdout=PIPE, stderr=PIPE) - return os.waitpid(p.pid, 0)[1] - -def get_mime_type(file_path): - """ - Attempts to get the mime type but will return prematurely if the process - takes longer than 5 seconds. Note that this function should only be called - for files which do not have a mp3/ogg/flac extension. - """ - - command = ['timeout', '5', 'file', '-b', '--mime-type', file_path] - return get_process_output(command) - -def duplicate_file(file_path): - """ - Makes a duplicate of the file and returns the path of this duplicate file. - """ - fsrc = open(file_path, 'r') - fdst = tempfile.NamedTemporaryFile(delete=False) - - logger.info("Copying %s to %s" % (file_path, fdst.name)) - - shutil.copyfileobj(fsrc, fdst) - - fsrc.close() - fdst.close() - - return fdst.name - -def get_file_type(file_path): - file_type = None - if re.search(r'mp3$', file_path, re.IGNORECASE): - file_type = 'mp3' - elif re.search(r'og(g|a)$', file_path, re.IGNORECASE): - file_type = 'vorbis' - elif re.search(r'm4a$', file_path, re.IGNORECASE): - file_type = 'mp4' - elif re.search(r'flac$', file_path, re.IGNORECASE): - file_type = 'flac' - else: - mime_type = get_mime_type(file_path) - if 'mpeg' in mime_type: - file_type = 'mp3' - elif 'ogg' in mime_type: - file_type = 'vorbis' - elif 'mp4' in mime_type: - file_type = 'mp4' - elif 'flac' in mime_type: - file_type = 'flac' - - return file_type - - -def calculate_replay_gain(file_path): - """ - This function accepts files of type mp3/ogg/flac and returns a calculated - ReplayGain value in dB. - If the value cannot be calculated for some reason, then we default to 0 - (Unity Gain). - - http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_1.0_specification - """ - - try: - """ - Making a duplicate is required because the ReplayGain extraction utilities we use - make unwanted modifications to the file. - """ - - search = None - temp_file_path = duplicate_file(file_path) - - file_type = get_file_type(file_path) - nice_level = '19' - - if file_type: - if file_type == 'mp3': - if run_process(['which', 'mp3gain']) == 0: - command = ['nice', '-n', nice_level, 'mp3gain', '-q', temp_file_path] - out = get_process_output(command) - search = re.search(r'Recommended "Track" dB change: (.*)', \ - out) - else: - logger.warn("mp3gain not found") - elif file_type == 'vorbis': - if run_process(['which', 'ogginfo']) == 0 and \ - run_process(['which', 'vorbisgain']) == 0: - command = ['nice', '-n', nice_level, 'vorbisgain', '-q', '-f', temp_file_path] - run_process(command) - - out = get_process_output(['ogginfo', temp_file_path]) - search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out) - else: - logger.warn("vorbisgain/ogginfo not found") - elif file_type == 'mp4': - if run_process(['which', 'aacgain']) == 0: - command = ['nice', '-n', nice_level, 'aacgain', '-q', temp_file_path] - out = get_process_output(command) - search = re.search(r'Recommended "Track" dB change: (.*)', \ - out) - else: - logger.warn("aacgain not found") - elif file_type == 'flac': - if run_process(['which', 'metaflac']) == 0: - - command = ['nice', '-n', nice_level, 'metaflac', \ - '--add-replay-gain', temp_file_path] - run_process(command) - - command = ['nice', '-n', nice_level, 'metaflac', \ - '--show-tag=REPLAYGAIN_TRACK_GAIN', \ - temp_file_path] - out = get_process_output(command) - search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out) - else: logger.warn("metaflac not found") - - except Exception, e: - logger.error(str(e)) - finally: - #no longer need the temp, file simply remove it. - try: os.remove(temp_file_path) - except: pass - - replay_gain = 0 - if search: - matches = search.groups() - if len(matches) == 1: - replay_gain = matches[0] - else: - logger.warn("Received more than 1 match in: '%s'" % str(matches)) - - return replay_gain - - -# Example of running from command line: -# python replay_gain.py /path/to/filename.mp3 -if __name__ == "__main__": - print calculate_replay_gain(sys.argv[1]) diff --git a/python_apps/pypo/media/update/replaygainupdater.py b/python_apps/pypo/media/update/replaygainupdater.py deleted file mode 100644 index c1123f4a8..000000000 --- a/python_apps/pypo/media/update/replaygainupdater.py +++ /dev/null @@ -1,86 +0,0 @@ -from threading import Thread - -import traceback -import os -import time -import logging - -from media.update import replaygain - -class ReplayGainUpdater(Thread): - """ - The purpose of the class is to query the server for a list of files which - do not have a ReplayGain value calculated. This class will iterate over the - list, calculate the values, update the server and repeat the process until - the server reports there are no files left. - - This class will see heavy activity right after a 2.1->2.2 upgrade since 2.2 - introduces ReplayGain normalization. A fresh install of Airtime 2.2 will - see this class not used at all since a file imported in 2.2 will - automatically have its ReplayGain value calculated. - """ - - @staticmethod - def start_reply_gain(apc): - me = ReplayGainUpdater(apc) - me.daemon = True - me.start() - - def __init__(self,apc): - Thread.__init__(self) - self.api_client = apc - self.logger = logging.getLogger() - - def main(self): - raw_response = self.api_client.list_all_watched_dirs() - if 'dirs' not in raw_response: - self.logger.error("Could not get a list of watched directories \ - with a dirs attribute. Printing full request:") - self.logger.error( raw_response ) - return - - directories = raw_response['dirs'] - - for dir_id, dir_path in directories.iteritems(): - try: - # keep getting few rows at a time for current music_dir (stor - # or watched folder). - total = 0 - while True: - # return a list of pairs where the first value is the - # file's database row id and the second value is the - # filepath - files = self.api_client.get_files_without_replay_gain_value(dir_id) - processed_data = [] - for f in files: - full_path = os.path.join(dir_path, f['fp']) - processed_data.append((f['id'], replaygain.calculate_replay_gain(full_path))) - total += 1 - - try: - if len(processed_data): - self.api_client.update_replay_gain_values(processed_data) - except Exception as e: - self.logger.error(e) - self.logger.debug(traceback.format_exc()) - - if len(files) == 0: break - self.logger.info("Processed: %d songs" % total) - - except Exception, e: - self.logger.error(e) - self.logger.debug(traceback.format_exc()) - def run(self): - while True: - try: - self.logger.info("Running replaygain updater") - self.main() - # Sleep for 5 minutes in case new files have been added - except Exception, e: - self.logger.error('ReplayGainUpdater Exception: %s', traceback.format_exc()) - self.logger.error(e) - time.sleep(60 * 5) - -if __name__ == "__main__": - rgu = ReplayGainUpdater() - rgu.main() diff --git a/python_apps/pypo/media/update/silananalyzer.py b/python_apps/pypo/media/update/silananalyzer.py deleted file mode 100644 index e0b98a5b0..000000000 --- a/python_apps/pypo/media/update/silananalyzer.py +++ /dev/null @@ -1,94 +0,0 @@ -from threading import Thread - -import traceback -import time -import subprocess -import json - - -class SilanAnalyzer(Thread): - """ - The purpose of the class is to query the server for a list of files which - do not have a Silan value calculated. This class will iterate over the - list calculate the values, update the server and repeat the process until - the server reports there are no files left. - """ - - @staticmethod - def start_silan(apc, logger): - me = SilanAnalyzer(apc, logger) - me.start() - - def __init__(self, apc, logger): - Thread.__init__(self) - self.api_client = apc - self.logger = logger - - def main(self): - while True: - # keep getting few rows at a time for current music_dir (stor - # or watched folder). - total = 0 - - # return a list of pairs where the first value is the - # file's database row id and the second value is the - # filepath - files = self.api_client.get_files_without_silan_value() - total_files = len(files) - if total_files == 0: return - processed_data = [] - for f in files: - full_path = f['fp'] - # silence detect(set default queue in and out) - try: - data = {} - command = ['nice', '-n', '19', 'silan', '-b', '-f', 'JSON', full_path] - try: - proc = subprocess.Popen(command, stdout=subprocess.PIPE) - comm = proc.communicate() - if len(comm): - out = comm[0].strip('\r\n') - info = json.loads(out) - try: data['length'] = str('{0:f}'.format(info['file duration'])) - except: pass - try: data['cuein'] = str('{0:f}'.format(info['sound'][0][0])) - except: pass - try: data['cueout'] = str('{0:f}'.format(info['sound'][-1][1])) - except: pass - except Exception, e: - self.logger.warn(str(command)) - self.logger.warn(e) - processed_data.append((f['id'], data)) - total += 1 - if total % 5 == 0: - self.logger.info("Total %s / %s files has been processed.." % (total, total_files)) - except Exception, e: - self.logger.error(e) - self.logger.error(traceback.format_exc()) - - try: - self.api_client.update_cue_values_by_silan(processed_data) - except Exception ,e: - self.logger.error(e) - self.logger.error(traceback.format_exc()) - - self.logger.info("Processed: %d songs" % total) - - def run(self): - while True: - try: - self.logger.info("Running Silan analyzer") - self.main() - except Exception, e: - self.logger.error('Silan Analyzer Exception: %s', traceback.format_exc()) - self.logger.error(e) - self.logger.info("Sleeping for 5...") - time.sleep(60 * 5) - -if __name__ == "__main__": - from api_clients import api_client - import logging - logging.basicConfig(level=logging.DEBUG) - api_client = api_client.AirtimeApiClient() - SilanAnalyzer.start_silan(api_client, logging) - diff --git a/python_apps/pypo/pypocli.py b/python_apps/pypo/pypocli.py index e0208e83a..62d95ad6c 100644 --- a/python_apps/pypo/pypocli.py +++ b/python_apps/pypo/pypocli.py @@ -27,9 +27,6 @@ from pypomessagehandler import PypoMessageHandler from pypoliquidsoap import PypoLiquidsoap from timeout import ls_timeout -from media.update.replaygainupdater import ReplayGainUpdater -from media.update.silananalyzer import SilanAnalyzer - from configobj import ConfigObj # custom imports @@ -250,10 +247,6 @@ if __name__ == '__main__': g.test_api() sys.exit(0) - - ReplayGainUpdater.start_reply_gain(api_client) - SilanAnalyzer.start_silan(api_client, logger) - pypoFetch_q = Queue() recorder_q = Queue() pypoPush_q = Queue()