From 86ee30df0d177f9103a7c4e7489cbf06aaa62838 Mon Sep 17 00:00:00 2001 From: Yevgeny Bezman Date: Sat, 15 Feb 2014 15:37:10 +0200 Subject: [PATCH 01/26] replaygain: modified current implementation to fit a backend framework in anticipation of other implementations --- beetsplug/replaygain.py | 335 ++++++++++++++++++++++++---------------- 1 file changed, 202 insertions(+), 133 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 6b4c2f407..f81f142c6 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -15,6 +15,8 @@ import logging import subprocess import os +import collections +import itertools from beets import ui from beets.plugins import BeetsPlugin @@ -23,7 +25,6 @@ from beets import config log = logging.getLogger('beets') -SAMPLE_MAX = 1 << 15 class ReplayGainError(Exception): """Raised when an error occurs during mp3gain/aacgain execution. @@ -45,52 +46,27 @@ def call(args): # http://code.google.com/p/beets/issues/detail?id=499 raise ReplayGainError("argument encoding failed") -def parse_tool_output(text): - """Given the tab-delimited output from an invocation of mp3gain - or aacgain, parse the text and return a list of dictionaries - containing information about each analyzed file. - """ - out = [] - for line in text.split('\n'): - parts = line.split('\t') - if len(parts) != 6 or parts[0] == 'File': - continue - out.append({ - 'file': parts[0], - 'mp3gain': int(parts[1]), - 'gain': float(parts[2]), - 'peak': float(parts[3]) / SAMPLE_MAX, - 'maxgain': int(parts[4]), - 'mingain': int(parts[5]), - }) - return out +class Backend(object): + Gain = collections.namedtuple("Gain", "gain peak") + AlbumGain = collections.namedtuple("AlbumGain", "album_gain track_gains") -class ReplayGainPlugin(BeetsPlugin): - """Provides ReplayGain analysis. - """ - def __init__(self): - super(ReplayGainPlugin, self).__init__() - self.import_stages = [self.imported] + def __init__(self, config): + pass - self.config.add({ - 'overwrite': False, - 'albumgain': False, - 'noclip': True, - 'apply_gain': False, - 'targetlevel': 89, - 'auto': True, - 'command': u'', - }) + def compute_track_gain(self, items): + raise NotImplementedError() - self.overwrite = self.config['overwrite'].get(bool) - self.albumgain = self.config['albumgain'].get(bool) - self.noclip = self.config['noclip'].get(bool) - self.apply_gain = self.config['apply_gain'].get(bool) - target_level = self.config['targetlevel'].as_number() - self.gain_offset = int(target_level - 89) - self.automatic = self.config['auto'].get(bool) - self.command = self.config['command'].get(unicode) + def compute_album_gain(self, album): + # TODO: implement album gain in terms of track gain of the individual tracks which can be used for any backend. + raise NotImplementedError() + +class CommandBackend(Backend): + def __init__(self, config): + super(CommandBackend, self).__init__(config) + + self.command = config["command"].get(unicode) + if self.command: # Explicit executable path. if not os.path.isfile(self.command): @@ -111,83 +87,40 @@ class ReplayGainPlugin(BeetsPlugin): raise ui.UserError( 'no replaygain command found: install mp3gain or aacgain' ) + + self.noclip = config['noclip'].get(bool) + target_level = config['targetlevel'].as_number() + self.gain_offset = int(target_level - 89) - def imported(self, session, task): - """Our import stage function.""" - if not self.automatic: - return + def compute_track_gain(self, items): + supported_items = filter(self.format_supported, items) + output = self.compute_gain(supported_items, False) + return output - if task.is_album: - album = session.lib.get_album(task.album_id) - items = list(album.items()) - else: - items = [task.item] + def compute_album_gain(self, album): + # TODO: What should be done when not all tracks in the album are supported? - results = self.compute_rgain(items, task.is_album) - if results: - self.store_gain(session.lib, items, results, - album if task.is_album else None) + supported_items = filter(self.format_supported, album.items()) + if len(supported_items) != len(album.items()): + return Backend.AlbumGain(None, []) - def commands(self): - """Provide a ReplayGain command.""" - def func(lib, opts, args): - write = config['import']['write'].get(bool) - - if opts.album: - # Analyze albums. - for album in lib.albums(ui.decargs(args)): - log.info(u'analyzing {0} - {1}'.format(album.albumartist, - album.album)) - items = list(album.items()) - results = self.compute_rgain(items, True) - if results: - self.store_gain(lib, items, results, album) - - if write: - for item in items: - item.write() - - else: - # Analyze individual tracks. - for item in lib.items(ui.decargs(args)): - log.info(u'analyzing {0} - {1}'.format(item.artist, - item.title)) - results = self.compute_rgain([item], False) - if results: - self.store_gain(lib, [item], results, None) - - if write: - item.write() - - cmd = ui.Subcommand('replaygain', help='analyze for ReplayGain') - cmd.parser.add_option('-a', '--album', action='store_true', - help='analyze albums instead of tracks') - cmd.func = func - return [cmd] - - def requires_gain(self, item, album=False): - """Does the gain need to be computed?""" + output = self.compute_gain(supported_items, True) + return Backend.AlbumGain(output[-1], output[:-1]) + + def format_supported(self, item): if 'mp3gain' in self.command and item.format != 'MP3': return False elif 'aacgain' in self.command and item.format not in ('MP3', 'AAC'): return False - return self.overwrite or \ - (not item.rg_track_gain or not item.rg_track_peak) or \ - ((not item.rg_album_gain or not item.rg_album_peak) and \ - album) + return True + + def compute_gain(self, items, is_album): + if len(items) == 0: + return [] - def compute_rgain(self, items, album=False): """Compute ReplayGain values and return a list of results dictionaries as given by `parse_tool_output`. """ - # Skip calculating gain only when *all* files don't need - # recalculation. This way, if any file among an album's tracks - # needs recalculation, we still get an accurate album gain - # value. - if all([not self.requires_gain(i, album) for i in items]): - log.debug(u'replaygain: no gain to compute') - return - # Construct shell command. The "-o" option makes the output # easily parseable (tab-delimited). "-s s" forces gain # recalculation even if tags are already present and disables @@ -201,44 +134,180 @@ class ReplayGainPlugin(BeetsPlugin): else: # Disable clipping warning. cmd = cmd + ['-c'] - if self.apply_gain: - # Lossless audio adjustment. - cmd = cmd + ['-a' if album and self.albumgain else '-r'] + cmd = cmd + ['-a' if is_album else '-r'] cmd = cmd + ['-d', str(self.gain_offset)] cmd = cmd + [syspath(i.path) for i in items] log.debug(u'replaygain: analyzing {0} files'.format(len(items))) try: + log.debug(u"replaygain: executing %s" % " ".join(cmd)) output = call(cmd) except ReplayGainError as exc: log.warn(u'replaygain: analysis failed ({0})'.format(exc)) return log.debug(u'replaygain: analysis finished') - results = parse_tool_output(output) + results = self.parse_tool_output(output, len(items) + (1 if is_album else 0)) return results + - def store_gain(self, lib, items, rgain_infos, album=None): - """Store computed ReplayGain values to the Items and the Album - (if it is provided). + def parse_tool_output(self, text, num_lines): + """Given the tab-delimited output from an invocation of mp3gain + or aacgain, parse the text and return a list of dictionaries + containing information about each analyzed file. """ - for item, info in zip(items, rgain_infos): - item.rg_track_gain = info['gain'] - item.rg_track_peak = info['peak'] - item.store() + out = [] + for line in text.split('\n')[1:num_lines + 1]: + parts = line.split('\t') + d = { + 'file': parts[0], + 'mp3gain': int(parts[1]), + 'gain': float(parts[2]), + 'peak': float(parts[3]) / (1 << 15), + 'maxgain': int(parts[4]), + 'mingain': int(parts[5]), + + } + out.append(Backend.Gain(d['gain'], d['peak'])) + return out + + + @staticmethod + def initialize_config(config): + config.add({ + 'command': u"", + 'noclip': True, + 'targetlevel': 89}) + + + +class ReplayGainPlugin(BeetsPlugin): + """Provides ReplayGain analysis. + """ + + BACKENDS = { "command" : CommandBackend, # "gstreamer" : GStreamerBackend + } + + def __init__(self): + super(ReplayGainPlugin, self).__init__() + self.import_stages = [self.imported] + + self.config.add({ + 'overwrite': False, + 'auto': True, + 'backend': u'', + }) + + self.overwrite = self.config['overwrite'].get(bool) + self.automatic = self.config['auto'].get(bool) + backend_name = self.config['backend'].get(unicode) + if backend_name not in ReplayGainPlugin.BACKENDS: + raise ui.UserError("Selected backend %s is not supported, please select one of: %s" % (backend_name, ReplayGainPlugin.BACKENDS.keys())) + self.backend = ReplayGainPlugin.BACKENDS[backend_name].initialize_config(self.config) + + self.backend_instance = ReplayGainPlugin.BACKENDS[backend_name](self.config) + + + def track_requires_gain(self, item): + return self.overwrite or \ + (not item.rg_track_gain or not item.rg_track_peak) + + + def album_requires_gain(self, album): + # Skip calculating gain only when *all* files don't need + # recalculation. This way, if any file among an album's tracks + # needs recalculation, we still get an accurate album gain + # value. + return self.overwrite or \ + not all([not item.rg_album_gain or not item.rg_album_peak for item in album.items()]) + + + def store_track_gain(self, item, track_gain): + item.rg_track_gain = track_gain.gain + item.rg_track_peak = track_gain.peak + item.store() + + log.debug(u'replaygain: applied track gain {0}, peak {1}'.format( + item.rg_track_gain, + item.rg_track_peak + )) + + def store_album_gain(self, album, album_gain): + album.rg_album_gain = album_gain.gain + album.rg_album_peak = album_gain.peak + album.store() + + log.debug(u'replaygain: applied album gain {0}, peak {1}'.format( + album.rg_album_gain, + album.rg_album_peak + )) + + + def handle_album(self, album, write): + if not self.album_requires_gain(album): + log.info(u'Skipping album {0} - {1}'.format(album.albumartist, + album.album)) + + album_gain = self.backend_instance.compute_album_gain(album) + if len(album_gain.track_gains) != len(album.items()): + log.warn("ReplayGain backend failed for some tracks in album %s - %s" % (album.albumartist, album.album)) + return + + self.store_album_gain(album, album_gain.album_gain) + for item, track_gain in itertools.izip(album.items(), album_gain.track_gains): + self.store_track_gain(item, track_gain) + if write: + item.write() + + + + def handle_track(self, item, write): + if not self.track_requires_gain(item): + log.info(u'Skipping track {0} - {1}'.format(item.artist, + item.title)) + + track_gains = self.backend_instance.compute_track_gain([item]) + if len(track_gains) != 1: + log.warn("ReplayGain backend failed for track %s - %s" % (item.artist, item.title)) + return + + self.store_track_gain(item, track_gains[0], write) + + + def imported(self, session, task): + """Our import stage function.""" + if not self.automatic: + return + + if task.is_album: + album = session.lib.get_album(task.album_id) + self.handle_album(album, False) + else: + self.handle_track(task.item, False) + + + def commands(self): + """Provide a ReplayGain command.""" + def func(lib, opts, args): + write = config['import']['write'].get(bool) + + if opts.album: + for album in lib.albums(ui.decargs(args)): + # log.info(u'analyzing {0} - {1}'.format(album.albumartist, + # album.album)) + self.handle_album(album, write) + + else: + # log.info(u'analyzing {0} - {1}'.format(item.artist, + # item.title)) + for item in lib.items(ui.decargs(args)): + self.handle_track(item, write) + + cmd = ui.Subcommand('replaygain', help='analyze for ReplayGain') + cmd.parser.add_option('-a', '--album', action='store_true', + help='analyze albums instead of tracks') + cmd.func = func + return [cmd] + - log.debug(u'replaygain: applied track gain {0}, peak {1}'.format( - item.rg_track_gain, - item.rg_track_peak - )) - if album and self.albumgain: - assert len(rgain_infos) == len(items) + 1 - album_info = rgain_infos[-1] - album.rg_album_gain = album_info['gain'] - album.rg_album_peak = album_info['peak'] - log.debug(u'replaygain: applied album gain {0}, peak {1}'.format( - album.rg_album_gain, - album.rg_album_peak - )) - album.store() From 9581b91e10de9a1586f6e2966c1747ec87978abf Mon Sep 17 00:00:00 2001 From: Yevgeny Bezman Date: Sat, 8 Mar 2014 10:59:39 +0200 Subject: [PATCH 02/26] replaygain: Added gstreamer support, fixed some small bugs --- beetsplug/replaygain.py | 192 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 3 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index f81f142c6..0dd4e109d 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -17,6 +17,9 @@ import subprocess import os import collections import itertools +import sys +import copy +import time from beets import ui from beets.plugins import BeetsPlugin @@ -178,6 +181,182 @@ class CommandBackend(Backend): 'command': u"", 'noclip': True, 'targetlevel': 89}) + +class GStreamerBackend(object): + def __init__(self, config): + self._src = Gst.ElementFactory.make("filesrc", "src") + self._decbin = Gst.ElementFactory.make("decodebin", "decbin") + self._conv = Gst.ElementFactory.make("audioconvert", "conv") + self._res = Gst.ElementFactory.make("audioresample", "res") + self._rg = Gst.ElementFactory.make("rganalysis", "rg") + self._rg.set_property("forced", True) + self._sink = Gst.ElementFactory.make("fakesink", "sink") + + self._pipe = Gst.Pipeline() + self._pipe.add(self._src) + self._pipe.add(self._decbin) + self._pipe.add(self._conv) + self._pipe.add(self._res) + self._pipe.add(self._rg) + self._pipe.add(self._sink) + + self._src.link(self._decbin) + self._conv.link(self._res) + self._res.link(self._rg) + self._rg.link(self._sink) + + self._bus = self._pipe.get_bus() + self._bus.add_signal_watch() + self._bus.connect("message::eos", self._on_eos) + self._bus.connect("message::error", self._on_error) + self._bus.connect("message::tag", self._on_tag) + self._decbin.connect("pad-added", self._on_pad_added) + self._decbin.connect("pad-removed", self._on_pad_removed) + + self._main_loop = GObject.MainLoop() + + self._files = [] + + def compute(self, files, album): + if len(self._files) != 0: + raise Exception() + + + self._files = list(files) + + if len(self._files) == 0: + return + + self._file_tags = collections.defaultdict(dict) + + if album: + self._rg.set_property("num-tracks", len(self._files)) + + if self._set_first_file(): + self._main_loop.run() + + def compute_track_gain(self, items): + self.compute(items, False) + if len(self._file_tags) != len(items): + raise Exception() + + ret = [] + for item in items: + ret.append(Backend.Gain(self._file_tags[item]["TRACK_GAIN"], self._file_tags[item]["TRACK_PEAK"])) + + return ret + + def compute_album_gain(self, album): + items = list(album.items()) + self.compute(items, True) + if len(self._file_tags) != len(items): + raise Exception() + + ret = [] + for item in items: + ret.append(Backend.Gain(self._file_tags[item]["TRACK_GAIN"], self._file_tags[item]["TRACK_PEAK"])) + + last_tags = self._file_tags[items[-1]] + return Backend.AlbumGain(Backend.Gain(last_tags["ALBUM_GAIN"], last_tags["ALBUM_PEAK"]), ret) + + def close(self): + self._bus.remove_signal_watch() + + def _on_eos(self, bus, message): + if not self._set_next_file(): + ret = self._pipe.set_state(Gst.State.NULL) + self._main_loop.quit() + + + def _on_error(self, bus, message): + self._pipe.set_state(Gst.State.NULL) + self._main_loop.quit() + err, debug = message.parse_error() + raise Exception("Error %s - %s on file %s" % (err, debug, self._src.get_property("location"))) + + def _on_tag(self, bus, message): + tags = message.parse_tag() + + def handle_tag(taglist, tag, userdata): + if tag == Gst.TAG_TRACK_GAIN: + self._file_tags[self._file]["TRACK_GAIN"] = taglist.get_double(tag)[1] + elif tag == Gst.TAG_TRACK_PEAK: + self._file_tags[self._file]["TRACK_PEAK"] = taglist.get_double(tag)[1] + elif tag == Gst.TAG_ALBUM_GAIN: + self._file_tags[self._file]["ALBUM_GAIN"] = taglist.get_double(tag)[1] + elif tag == Gst.TAG_ALBUM_PEAK: + self._file_tags[self._file]["ALBUM_PEAK"] = taglist.get_double(tag)[1] + elif tag == Gst.TAG_REFERENCE_LEVEL: + self._file_tags[self._file]["REFERENCE_LEVEL"] = taglist.get_double(tag)[1] + + tags.foreach(handle_tag, None) + + + def _set_first_file(self): + if len(self._files) == 0: + return False + + self._file = self._files.pop(0) + self._src.set_property("location", syspath(self._file.path)) + + self._pipe.set_state(Gst.State.PLAYING) + + return True + + def _set_file(self): + if len(self._files) == 0: + return False + + self._file = self._files.pop(0) + + self._decbin.unlink(self._conv) + self._decbin.set_state(Gst.State.READY) + + self._src.set_state(Gst.State.READY) + self._src.set_property("location", syspath(self._file.path)) + + self._src.sync_state_with_parent() + self._src.get_state(Gst.CLOCK_TIME_NONE) + self._decbin.sync_state_with_parent() + self._decbin.get_state(Gst.CLOCK_TIME_NONE) + + return True + + + def _set_next_file(self): + self._pipe.set_state(Gst.State.PAUSED) + self._pipe.get_state(Gst.CLOCK_TIME_NONE) + + ret = self._set_file() + if ret: + self._pipe.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, 0) + self._pipe.set_state(Gst.State.PLAYING) + + return ret + + + def _on_pad_added(self, decbin, pad): + sink_pad = self._conv.get_compatible_pad(pad, None) + if sink_pad is None: + raise Exception() + + pad.link(sink_pad) + + + def _on_pad_removed(self, decbin, pad): + peer = pad.get_peer() + if peer is not None: + raise Exception() + + @staticmethod + def initialize_config(config): + global GObject, Gst + + import gi + gi.require_version('Gst', '1.0') + from gi.repository import GObject, Gst + GObject.threads_init() + Gst.init([sys.argv[0]]) @@ -185,7 +364,9 @@ class ReplayGainPlugin(BeetsPlugin): """Provides ReplayGain analysis. """ - BACKENDS = { "command" : CommandBackend, # "gstreamer" : GStreamerBackend + BACKENDS = { + "command" : CommandBackend, + "gstreamer" : GStreamerBackend } def __init__(self): @@ -219,7 +400,7 @@ class ReplayGainPlugin(BeetsPlugin): # needs recalculation, we still get an accurate album gain # value. return self.overwrite or \ - not all([not item.rg_album_gain or not item.rg_album_peak for item in album.items()]) + any([not item.rg_album_gain or not item.rg_album_peak for item in album.items()]) def store_track_gain(self, item, track_gain): @@ -247,6 +428,7 @@ class ReplayGainPlugin(BeetsPlugin): if not self.album_requires_gain(album): log.info(u'Skipping album {0} - {1}'.format(album.albumartist, album.album)) + return album_gain = self.backend_instance.compute_album_gain(album) if len(album_gain.track_gains) != len(album.items()): @@ -257,6 +439,7 @@ class ReplayGainPlugin(BeetsPlugin): for item, track_gain in itertools.izip(album.items(), album_gain.track_gains): self.store_track_gain(item, track_gain) if write: + print "WRITING" item.write() @@ -265,13 +448,16 @@ class ReplayGainPlugin(BeetsPlugin): if not self.track_requires_gain(item): log.info(u'Skipping track {0} - {1}'.format(item.artist, item.title)) + return track_gains = self.backend_instance.compute_track_gain([item]) if len(track_gains) != 1: log.warn("ReplayGain backend failed for track %s - %s" % (item.artist, item.title)) return - self.store_track_gain(item, track_gains[0], write) + self.store_track_gain(item, track_gains[0]) + if write: + item.write() def imported(self, session, task): From d2c6d00b7b43208ba705c49ced4c981bf651a1e3 Mon Sep 17 00:00:00 2001 From: Yevgeny Bezman Date: Sat, 8 Mar 2014 12:59:15 +0200 Subject: [PATCH 03/26] replaygain: Added some informative prints, removed hardcoded debug print --- beetsplug/replaygain.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 0dd4e109d..1dd2e6e35 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -429,6 +429,9 @@ class ReplayGainPlugin(BeetsPlugin): log.info(u'Skipping album {0} - {1}'.format(album.albumartist, album.album)) return + + log.info(u'analyzing {0} - {1}'.format(album.albumartist, + album.album)) album_gain = self.backend_instance.compute_album_gain(album) if len(album_gain.track_gains) != len(album.items()): @@ -439,7 +442,6 @@ class ReplayGainPlugin(BeetsPlugin): for item, track_gain in itertools.izip(album.items(), album_gain.track_gains): self.store_track_gain(item, track_gain) if write: - print "WRITING" item.write() @@ -449,6 +451,9 @@ class ReplayGainPlugin(BeetsPlugin): log.info(u'Skipping track {0} - {1}'.format(item.artist, item.title)) return + + log.info(u'analyzing {0} - {1}'.format(item.artist, + item.title)) track_gains = self.backend_instance.compute_track_gain([item]) if len(track_gains) != 1: @@ -479,13 +484,9 @@ class ReplayGainPlugin(BeetsPlugin): if opts.album: for album in lib.albums(ui.decargs(args)): - # log.info(u'analyzing {0} - {1}'.format(album.albumartist, - # album.album)) self.handle_album(album, write) else: - # log.info(u'analyzing {0} - {1}'.format(item.artist, - # item.title)) for item in lib.items(ui.decargs(args)): self.handle_track(item, write) From 2369122075917138078ff3b0cb04f4665e664a64 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 8 Mar 2014 13:32:53 -0800 Subject: [PATCH 04/26] minor Python style; move namedtuples to module --- beetsplug/replaygain.py | 185 +++++++++++++++++++++++----------------- 1 file changed, 106 insertions(+), 79 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 1dd2e6e35..4753e43d2 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2013, Fabrice Laporte. +# Copyright 2014, Fabrice Laporte, Yevgeny Bezman, and Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -18,8 +18,6 @@ import os import collections import itertools import sys -import copy -import time from beets import ui from beets.plugins import BeetsPlugin @@ -29,6 +27,8 @@ from beets import config log = logging.getLogger('beets') +# Utilities. + class ReplayGainError(Exception): """Raised when an error occurs during mp3gain/aacgain execution. """ @@ -49,27 +49,36 @@ def call(args): # http://code.google.com/p/beets/issues/detail?id=499 raise ReplayGainError("argument encoding failed") -class Backend(object): - Gain = collections.namedtuple("Gain", "gain peak") - AlbumGain = collections.namedtuple("AlbumGain", "album_gain track_gains") + +# Backend base and plumbing classes. + +Gain = collections.namedtuple("Gain", "gain peak") +AlbumGain = collections.namedtuple("AlbumGain", "album_gain track_gains") + +class Backend(object): + """An abstract class representing engine for calculating RG values. + """ def __init__(self, config): - pass + """Initialize the backend with the configuration view for the + plugin. + """ def compute_track_gain(self, items): raise NotImplementedError() def compute_album_gain(self, album): - # TODO: implement album gain in terms of track gain of the individual tracks which can be used for any backend. + # TODO: implement album gain in terms of track gain of the + # individual tracks which can be used for any backend. raise NotImplementedError() +# mpgain/aacgain CLI tool backend. + class CommandBackend(Backend): def __init__(self, config): - super(CommandBackend, self).__init__(config) - self.command = config["command"].get(unicode) - + if self.command: # Explicit executable path. if not os.path.isfile(self.command): @@ -90,7 +99,7 @@ class CommandBackend(Backend): raise ui.UserError( 'no replaygain command found: install mp3gain or aacgain' ) - + self.noclip = config['noclip'].get(bool) target_level = config['targetlevel'].as_number() self.gain_offset = int(target_level - 89) @@ -101,15 +110,16 @@ class CommandBackend(Backend): return output def compute_album_gain(self, album): - # TODO: What should be done when not all tracks in the album are supported? + # TODO: What should be done when not all tracks in the album are + # supported? supported_items = filter(self.format_supported, album.items()) if len(supported_items) != len(album.items()): - return Backend.AlbumGain(None, []) + return AlbumGain(None, []) output = self.compute_gain(supported_items, True) - return Backend.AlbumGain(output[-1], output[:-1]) - + return AlbumGain(output[-1], output[:-1]) + def format_supported(self, item): if 'mp3gain' in self.command and item.format != 'MP3': return False @@ -149,10 +159,10 @@ class CommandBackend(Backend): log.warn(u'replaygain: analysis failed ({0})'.format(exc)) return log.debug(u'replaygain: analysis finished') - results = self.parse_tool_output(output, len(items) + (1 if is_album else 0)) + results = self.parse_tool_output(output, + len(items) + (1 if is_album else 0)) return results - def parse_tool_output(self, text, num_lines): """Given the tab-delimited output from an invocation of mp3gain @@ -171,16 +181,19 @@ class CommandBackend(Backend): 'mingain': int(parts[5]), } - out.append(Backend.Gain(d['gain'], d['peak'])) + out.append(Gain(d['gain'], d['peak'])) return out - @staticmethod def initialize_config(config): config.add({ 'command': u"", 'noclip': True, - 'targetlevel': 89}) + 'targetlevel': 89, + }) + + +# GStreamer-based backend. class GStreamerBackend(object): def __init__(self, config): @@ -191,7 +204,7 @@ class GStreamerBackend(object): self._rg = Gst.ElementFactory.make("rganalysis", "rg") self._rg.set_property("forced", True) self._sink = Gst.ElementFactory.make("fakesink", "sink") - + self._pipe = Gst.Pipeline() self._pipe.add(self._src) self._pipe.add(self._decbin) @@ -199,12 +212,12 @@ class GStreamerBackend(object): self._pipe.add(self._res) self._pipe.add(self._rg) self._pipe.add(self._sink) - + self._src.link(self._decbin) self._conv.link(self._res) self._res.link(self._rg) self._rg.link(self._sink) - + self._bus = self._pipe.get_bus() self._bus.add_signal_watch() self._bus.connect("message::eos", self._on_eos) @@ -212,7 +225,7 @@ class GStreamerBackend(object): self._bus.connect("message::tag", self._on_tag) self._decbin.connect("pad-added", self._on_pad_added) self._decbin.connect("pad-removed", self._on_pad_removed) - + self._main_loop = GObject.MainLoop() self._files = [] @@ -223,7 +236,7 @@ class GStreamerBackend(object): self._files = list(files) - + if len(self._files) == 0: return @@ -231,7 +244,7 @@ class GStreamerBackend(object): if album: self._rg.set_property("num-tracks", len(self._files)) - + if self._set_first_file(): self._main_loop.run() @@ -242,7 +255,8 @@ class GStreamerBackend(object): ret = [] for item in items: - ret.append(Backend.Gain(self._file_tags[item]["TRACK_GAIN"], self._file_tags[item]["TRACK_PEAK"])) + ret.append(Gain(self._file_tags[item]["TRACK_GAIN"], + self._file_tags[item]["TRACK_PEAK"])) return ret @@ -254,51 +268,57 @@ class GStreamerBackend(object): ret = [] for item in items: - ret.append(Backend.Gain(self._file_tags[item]["TRACK_GAIN"], self._file_tags[item]["TRACK_PEAK"])) + ret.append(Gain(self._file_tags[item]["TRACK_GAIN"], + self._file_tags[item]["TRACK_PEAK"])) last_tags = self._file_tags[items[-1]] - return Backend.AlbumGain(Backend.Gain(last_tags["ALBUM_GAIN"], last_tags["ALBUM_PEAK"]), ret) + return AlbumGain(Gain(last_tags["ALBUM_GAIN"], + last_tags["ALBUM_PEAK"]), ret) def close(self): self._bus.remove_signal_watch() def _on_eos(self, bus, message): if not self._set_next_file(): - ret = self._pipe.set_state(Gst.State.NULL) + self._pipe.set_state(Gst.State.NULL) self._main_loop.quit() - def _on_error(self, bus, message): self._pipe.set_state(Gst.State.NULL) self._main_loop.quit() err, debug = message.parse_error() - raise Exception("Error %s - %s on file %s" % (err, debug, self._src.get_property("location"))) + raise Exception("Error %s - %s on file %s" % + (err, debug, self._src.get_property("location"))) def _on_tag(self, bus, message): tags = message.parse_tag() def handle_tag(taglist, tag, userdata): if tag == Gst.TAG_TRACK_GAIN: - self._file_tags[self._file]["TRACK_GAIN"] = taglist.get_double(tag)[1] + self._file_tags[self._file]["TRACK_GAIN"] = \ + taglist.get_double(tag)[1] elif tag == Gst.TAG_TRACK_PEAK: - self._file_tags[self._file]["TRACK_PEAK"] = taglist.get_double(tag)[1] + self._file_tags[self._file]["TRACK_PEAK"] = \ + taglist.get_double(tag)[1] elif tag == Gst.TAG_ALBUM_GAIN: - self._file_tags[self._file]["ALBUM_GAIN"] = taglist.get_double(tag)[1] + self._file_tags[self._file]["ALBUM_GAIN"] = \ + taglist.get_double(tag)[1] elif tag == Gst.TAG_ALBUM_PEAK: - self._file_tags[self._file]["ALBUM_PEAK"] = taglist.get_double(tag)[1] + self._file_tags[self._file]["ALBUM_PEAK"] = \ + taglist.get_double(tag)[1] elif tag == Gst.TAG_REFERENCE_LEVEL: - self._file_tags[self._file]["REFERENCE_LEVEL"] = taglist.get_double(tag)[1] - + self._file_tags[self._file]["REFERENCE_LEVEL"] = \ + taglist.get_double(tag)[1] + tags.foreach(handle_tag, None) - def _set_first_file(self): if len(self._files) == 0: return False - + self._file = self._files.pop(0) self._src.set_property("location", syspath(self._file.path)) - + self._pipe.set_state(Gst.State.PLAYING) return True @@ -306,48 +326,45 @@ class GStreamerBackend(object): def _set_file(self): if len(self._files) == 0: return False - + self._file = self._files.pop(0) - + self._decbin.unlink(self._conv) self._decbin.set_state(Gst.State.READY) - + self._src.set_state(Gst.State.READY) self._src.set_property("location", syspath(self._file.path)) - + self._src.sync_state_with_parent() self._src.get_state(Gst.CLOCK_TIME_NONE) self._decbin.sync_state_with_parent() self._decbin.get_state(Gst.CLOCK_TIME_NONE) - + return True - - + def _set_next_file(self): self._pipe.set_state(Gst.State.PAUSED) self._pipe.get_state(Gst.CLOCK_TIME_NONE) - + ret = self._set_file() if ret: self._pipe.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, 0) self._pipe.set_state(Gst.State.PLAYING) return ret - - + def _on_pad_added(self, decbin, pad): sink_pad = self._conv.get_compatible_pad(pad, None) if sink_pad is None: raise Exception() - + pad.link(sink_pad) - def _on_pad_removed(self, decbin, pad): peer = pad.get_peer() if peer is not None: raise Exception() - + @staticmethod def initialize_config(config): global GObject, Gst @@ -357,16 +374,18 @@ class GStreamerBackend(object): from gi.repository import GObject, Gst GObject.threads_init() Gst.init([sys.argv[0]]) - + +# Main plugin logic. + class ReplayGainPlugin(BeetsPlugin): """Provides ReplayGain analysis. """ BACKENDS = { - "command" : CommandBackend, - "gstreamer" : GStreamerBackend + "command" : CommandBackend, + "gstreamer": GStreamerBackend, } def __init__(self): @@ -383,31 +402,37 @@ class ReplayGainPlugin(BeetsPlugin): self.automatic = self.config['auto'].get(bool) backend_name = self.config['backend'].get(unicode) if backend_name not in ReplayGainPlugin.BACKENDS: - raise ui.UserError("Selected backend %s is not supported, please select one of: %s" % (backend_name, ReplayGainPlugin.BACKENDS.keys())) + raise ui.UserError( + u"Selected ReplayGain backend {0} is not supported. " + u"Please select one of: {1}".format( + backend_name, + u', '.join(ReplayGainPlugin.BACKENDS.keys()) + ) + ) self.backend = ReplayGainPlugin.BACKENDS[backend_name].initialize_config(self.config) - self.backend_instance = ReplayGainPlugin.BACKENDS[backend_name](self.config) + self.backend_instance = ReplayGainPlugin.BACKENDS[backend_name]( + self.config + ) - def track_requires_gain(self, item): return self.overwrite or \ (not item.rg_track_gain or not item.rg_track_peak) - def album_requires_gain(self, album): # Skip calculating gain only when *all* files don't need # recalculation. This way, if any file among an album's tracks # needs recalculation, we still get an accurate album gain # value. return self.overwrite or \ - any([not item.rg_album_gain or not item.rg_album_peak for item in album.items()]) - + any([not item.rg_album_gain or not item.rg_album_peak + for item in album.items()]) def store_track_gain(self, item, track_gain): item.rg_track_gain = track_gain.gain item.rg_track_peak = track_gain.peak item.store() - + log.debug(u'replaygain: applied track gain {0}, peak {1}'.format( item.rg_track_gain, item.rg_track_peak @@ -417,53 +442,59 @@ class ReplayGainPlugin(BeetsPlugin): album.rg_album_gain = album_gain.gain album.rg_album_peak = album_gain.peak album.store() - + log.debug(u'replaygain: applied album gain {0}, peak {1}'.format( album.rg_album_gain, album.rg_album_peak )) - def handle_album(self, album, write): if not self.album_requires_gain(album): log.info(u'Skipping album {0} - {1}'.format(album.albumartist, album.album)) return - + log.info(u'analyzing {0} - {1}'.format(album.albumartist, album.album)) album_gain = self.backend_instance.compute_album_gain(album) if len(album_gain.track_gains) != len(album.items()): - log.warn("ReplayGain backend failed for some tracks in album %s - %s" % (album.albumartist, album.album)) + log.warn( + u"ReplayGain backend failed " + u"for some tracks in album {0} - {1}".format( + album.albumartist, album.album + ) + ) return self.store_album_gain(album, album_gain.album_gain) - for item, track_gain in itertools.izip(album.items(), album_gain.track_gains): + for item, track_gain in itertools.izip(album.items(), + album_gain.track_gains): self.store_track_gain(item, track_gain) if write: item.write() - - def handle_track(self, item, write): if not self.track_requires_gain(item): log.info(u'Skipping track {0} - {1}'.format(item.artist, item.title)) return - + log.info(u'analyzing {0} - {1}'.format(item.artist, item.title)) track_gains = self.backend_instance.compute_track_gain([item]) if len(track_gains) != 1: - log.warn("ReplayGain backend failed for track %s - %s" % (item.artist, item.title)) + log.warn( + u"ReplayGain backend failed for track {0} - {1}".format( + item.artist, item.title + ) + ) return self.store_track_gain(item, track_gains[0]) if write: item.write() - def imported(self, session, task): """Our import stage function.""" @@ -476,7 +507,6 @@ class ReplayGainPlugin(BeetsPlugin): else: self.handle_track(task.item, False) - def commands(self): """Provide a ReplayGain command.""" def func(lib, opts, args): @@ -495,6 +525,3 @@ class ReplayGainPlugin(BeetsPlugin): help='analyze albums instead of tracks') cmd.func = func return [cmd] - - - From 08b2bff28d38d30a19b65d0a7251b0d168d6e2be Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 8 Mar 2014 13:37:57 -0800 Subject: [PATCH 05/26] eliminate GStreamer globals and initialize_config --- beetsplug/replaygain.py | 101 ++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 4753e43d2..4481e8175 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -77,6 +77,12 @@ class Backend(object): class CommandBackend(Backend): def __init__(self, config): + config.add({ + 'command': u"", + 'noclip': True, + 'targetlevel': 89, + }) + self.command = config["command"].get(unicode) if self.command: @@ -184,28 +190,20 @@ class CommandBackend(Backend): out.append(Gain(d['gain'], d['peak'])) return out - @staticmethod - def initialize_config(config): - config.add({ - 'command': u"", - 'noclip': True, - 'targetlevel': 89, - }) - # GStreamer-based backend. class GStreamerBackend(object): def __init__(self, config): - self._src = Gst.ElementFactory.make("filesrc", "src") - self._decbin = Gst.ElementFactory.make("decodebin", "decbin") - self._conv = Gst.ElementFactory.make("audioconvert", "conv") - self._res = Gst.ElementFactory.make("audioresample", "res") - self._rg = Gst.ElementFactory.make("rganalysis", "rg") + self._src = self.Gst.ElementFactory.make("filesrc", "src") + self._decbin = self.Gst.ElementFactory.make("decodebin", "decbin") + self._conv = self.Gst.ElementFactory.make("audioconvert", "conv") + self._res = self.Gst.ElementFactory.make("audioresample", "res") + self._rg = self.Gst.ElementFactory.make("rganalysis", "rg") self._rg.set_property("forced", True) - self._sink = Gst.ElementFactory.make("fakesink", "sink") + self._sink = self.Gst.ElementFactory.make("fakesink", "sink") - self._pipe = Gst.Pipeline() + self._pipe = self.Gst.Pipeline() self._pipe.add(self._src) self._pipe.add(self._decbin) self._pipe.add(self._conv) @@ -226,10 +224,24 @@ class GStreamerBackend(object): self._decbin.connect("pad-added", self._on_pad_added) self._decbin.connect("pad-removed", self._on_pad_removed) - self._main_loop = GObject.MainLoop() + self._main_loop = self.GObject.MainLoop() self._files = [] + def _import_gst(self): + """Import the necessary GObject-related modules and assign `Gst` + and `GObject` fields on this object. + """ + import gi + gi.require_version('Gst', '1.0') + + from gi.repository import GObject, Gst + GObject.threads_init() + Gst.init([sys.argv[0]]) + + self.GObject = GObject + self.Gst = Gst + def compute(self, files, album): if len(self._files) != 0: raise Exception() @@ -280,11 +292,11 @@ class GStreamerBackend(object): def _on_eos(self, bus, message): if not self._set_next_file(): - self._pipe.set_state(Gst.State.NULL) + self._pipe.set_state(self.Gst.State.NULL) self._main_loop.quit() def _on_error(self, bus, message): - self._pipe.set_state(Gst.State.NULL) + self._pipe.set_state(self.Gst.State.NULL) self._main_loop.quit() err, debug = message.parse_error() raise Exception("Error %s - %s on file %s" % @@ -294,19 +306,19 @@ class GStreamerBackend(object): tags = message.parse_tag() def handle_tag(taglist, tag, userdata): - if tag == Gst.TAG_TRACK_GAIN: + if tag == self.Gst.TAG_TRACK_GAIN: self._file_tags[self._file]["TRACK_GAIN"] = \ taglist.get_double(tag)[1] - elif tag == Gst.TAG_TRACK_PEAK: + elif tag == self.Gst.TAG_TRACK_PEAK: self._file_tags[self._file]["TRACK_PEAK"] = \ taglist.get_double(tag)[1] - elif tag == Gst.TAG_ALBUM_GAIN: + elif tag == self.Gst.TAG_ALBUM_GAIN: self._file_tags[self._file]["ALBUM_GAIN"] = \ taglist.get_double(tag)[1] - elif tag == Gst.TAG_ALBUM_PEAK: + elif tag == self.Gst.TAG_ALBUM_PEAK: self._file_tags[self._file]["ALBUM_PEAK"] = \ taglist.get_double(tag)[1] - elif tag == Gst.TAG_REFERENCE_LEVEL: + elif tag == self.Gst.TAG_REFERENCE_LEVEL: self._file_tags[self._file]["REFERENCE_LEVEL"] = \ taglist.get_double(tag)[1] @@ -319,7 +331,7 @@ class GStreamerBackend(object): self._file = self._files.pop(0) self._src.set_property("location", syspath(self._file.path)) - self._pipe.set_state(Gst.State.PLAYING) + self._pipe.set_state(self.Gst.State.PLAYING) return True @@ -330,26 +342,28 @@ class GStreamerBackend(object): self._file = self._files.pop(0) self._decbin.unlink(self._conv) - self._decbin.set_state(Gst.State.READY) + self._decbin.set_state(self.Gst.State.READY) - self._src.set_state(Gst.State.READY) + self._src.set_state(self.Gst.State.READY) self._src.set_property("location", syspath(self._file.path)) self._src.sync_state_with_parent() - self._src.get_state(Gst.CLOCK_TIME_NONE) + self._src.get_state(self.Gst.CLOCK_TIME_NONE) self._decbin.sync_state_with_parent() - self._decbin.get_state(Gst.CLOCK_TIME_NONE) + self._decbin.get_state(self.Gst.CLOCK_TIME_NONE) return True def _set_next_file(self): - self._pipe.set_state(Gst.State.PAUSED) - self._pipe.get_state(Gst.CLOCK_TIME_NONE) + self._pipe.set_state(self.Gst.State.PAUSED) + self._pipe.get_state(self.Gst.CLOCK_TIME_NONE) ret = self._set_file() if ret: - self._pipe.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, 0) - self._pipe.set_state(Gst.State.PLAYING) + self._pipe.seek_simple(self.Gst.Format.TIME, + self.Gst.SeekFlags.FLUSH, + 0) + self._pipe.set_state(self.Gst.State.PLAYING) return ret @@ -365,29 +379,17 @@ class GStreamerBackend(object): if peer is not None: raise Exception() - @staticmethod - def initialize_config(config): - global GObject, Gst - - import gi - gi.require_version('Gst', '1.0') - from gi.repository import GObject, Gst - GObject.threads_init() - Gst.init([sys.argv[0]]) - - # Main plugin logic. +BACKENDS = { + "command": CommandBackend, + "gstreamer": GStreamerBackend, +} + class ReplayGainPlugin(BeetsPlugin): """Provides ReplayGain analysis. """ - - BACKENDS = { - "command" : CommandBackend, - "gstreamer": GStreamerBackend, - } - def __init__(self): super(ReplayGainPlugin, self).__init__() self.import_stages = [self.imported] @@ -409,7 +411,6 @@ class ReplayGainPlugin(BeetsPlugin): u', '.join(ReplayGainPlugin.BACKENDS.keys()) ) ) - self.backend = ReplayGainPlugin.BACKENDS[backend_name].initialize_config(self.config) self.backend_instance = ReplayGainPlugin.BACKENDS[backend_name]( self.config From 71a447fac13470b492dc3b5226774373f082dc7b Mon Sep 17 00:00:00 2001 From: Yevgeny Bezman Date: Thu, 27 Mar 2014 15:37:25 +0200 Subject: [PATCH 06/26] replaygain: Update documentation to include some info regarding the GStreamer backend --- docs/plugins/replaygain.rst | 39 ++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/plugins/replaygain.rst b/docs/plugins/replaygain.rst index dc88cd9a1..b571052c7 100644 --- a/docs/plugins/replaygain.rst +++ b/docs/plugins/replaygain.rst @@ -9,8 +9,37 @@ playback levels. Installation ------------ -This plugin uses the `mp3gain`_ command-line tool or the `aacgain`_ fork -thereof. To get started, install this tool: +This plugin can use one of two backends to compute the ReplayGain values + +GStreamer +````````` + +This backend uses the popular `GStreamer`_ multimedia framework. +In order to use this backend, you will need to install the GStreamer library +as well as a set of plugins for handling your selection of audio files. +Some linux distributions don't package plugins for some popular formats in +their default repositories, and packages for those plugins need to be +downloaded from elsewhere or compiled from source. + +The minimal version of the GStreamer library supported by this backend is 1.0 +The 0.x branch (which is the default on some older distributions) is not +supported. + +.. _GStreamer: http://gstreamer.freedesktop.org/ + +Then enable the GStreamer backend of the ``replaygain`` plugin +(see :doc:`/reference/config`) add the following to your config file:: + + replaygain: + backend: gstreamer + + +MP3Gain-based command-line tools +```````````````````````````````` + +In order to use this backend, you will need to install the `mp3gain`_ +command-line tool or the `aacgain`_ fork thereof. To get started, install this +tool: * On Mac OS X, you can use `Homebrew`_. Type ``brew install aacgain``. * On Linux, `mp3gain`_ is probably in your repositories. On Debian or Ubuntu, @@ -21,11 +50,12 @@ thereof. To get started, install this tool: .. _aacgain: http://aacgain.altosdesign.com .. _Homebrew: http://mxcl.github.com/homebrew/ -Then enable the ``replaygain`` plugin (see :doc:`/reference/config`). If beets +Then enable the MP3gain backend of the ``replaygain`` plugin (see :doc:`/reference/config`). If beets doesn't automatically find the ``mp3gain`` or ``aacgain`` executable, you can configure the path explicitly like so:: replaygain: + backend: command command: /Applications/MacMP3Gain.app/Contents/Resources/aacgain Usage & Configuration @@ -35,6 +65,9 @@ The plugin will automatically analyze albums and individual tracks as you import them. It writes tags to each file according to the `ReplayGain`_ specification; if your player supports these tags, it can use them to do level adjustment. +Note that some of these options are backend specific and are not currently +supported on all backends. + By default, files that already have ReplayGain tags will not be re-analyzed. If you want to analyze *every* file on import, you can set the ``overwrite`` option for the plugin in your :doc:`configuration file `, like so:: From 2d9f40d62d84347383376b2894d077551d325aec Mon Sep 17 00:00:00 2001 From: Yevgeny Bezman Date: Sat, 29 Mar 2014 21:46:53 +0300 Subject: [PATCH 07/26] replaygain: fixed small errors introduced by previous refactor. Commented the code in key parts. --- beetsplug/replaygain.py | 62 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 4481e8175..721d431ef 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -110,11 +110,17 @@ class CommandBackend(Backend): target_level = config['targetlevel'].as_number() self.gain_offset = int(target_level - 89) + """ + Computes the track gain of the given tracks, returns a list of TrackGain objects + """ def compute_track_gain(self, items): supported_items = filter(self.format_supported, items) output = self.compute_gain(supported_items, False) return output + """ + Computes the album gain of the given album, returns an AlbumGain object + """ def compute_album_gain(self, album): # TODO: What should be done when not all tracks in the album are # supported? @@ -126,6 +132,9 @@ class CommandBackend(Backend): output = self.compute_gain(supported_items, True) return AlbumGain(output[-1], output[:-1]) + """ + Checks whether the given item is supported by the selected tool + """ def format_supported(self, item): if 'mp3gain' in self.command and item.format != 'MP3': return False @@ -133,6 +142,10 @@ class CommandBackend(Backend): return False return True + """ + Computes the track or album gain of a list of items, returns a list of TrackGain objects + When computing album gain, the last TrackGain object returned is the album gain + """ def compute_gain(self, items, is_album): if len(items) == 0: return [] @@ -195,11 +208,19 @@ class CommandBackend(Backend): class GStreamerBackend(object): def __init__(self, config): + self._import_gst() + + # Initialized a GStreamer pipeline of the form + # filesrc -> decodebin -> audioconvert -> audioresample -> rganalysis -> fakesink + # The connection between decodebin and audioconvert is handled dynamically after decodebin + # figures out the type of the input file. self._src = self.Gst.ElementFactory.make("filesrc", "src") self._decbin = self.Gst.ElementFactory.make("decodebin", "decbin") self._conv = self.Gst.ElementFactory.make("audioconvert", "conv") self._res = self.Gst.ElementFactory.make("audioresample", "res") self._rg = self.Gst.ElementFactory.make("rganalysis", "rg") + # We check which files need gain ourselves, so all files given to rganalsys should have their gain + # computed, even if it already exists. self._rg.set_property("forced", True) self._sink = self.Gst.ElementFactory.make("fakesink", "sink") @@ -221,6 +242,7 @@ class GStreamerBackend(object): self._bus.connect("message::eos", self._on_eos) self._bus.connect("message::error", self._on_error) self._bus.connect("message::tag", self._on_tag) + # Needed for handling the dynamic connection between decodebin and audioconvert self._decbin.connect("pad-added", self._on_pad_added) self._decbin.connect("pad-removed", self._on_pad_removed) @@ -236,6 +258,8 @@ class GStreamerBackend(object): gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst + # Thread initialization. The pipeline freezes if not initialized at this point. Not entirely sure why + # this is not handled by the framwork. GObject.threads_init() Gst.init([sys.argv[0]]) @@ -244,9 +268,9 @@ class GStreamerBackend(object): def compute(self, files, album): if len(self._files) != 0: + # Previous invocation did not consume all files raise Exception() - self._files = list(files) if len(self._files) == 0: @@ -263,6 +287,7 @@ class GStreamerBackend(object): def compute_track_gain(self, items): self.compute(items, False) if len(self._file_tags) != len(items): + # Some items did not recieve tags raise Exception() ret = [] @@ -276,6 +301,7 @@ class GStreamerBackend(object): items = list(album.items()) self.compute(items, True) if len(self._file_tags) != len(items): + # Some items did not receive tags raise Exception() ret = [] @@ -291,6 +317,8 @@ class GStreamerBackend(object): self._bus.remove_signal_watch() def _on_eos(self, bus, message): + # A file finished playing in all elements of the pipeline. The RG tags have already been propagated. + # If we don't have a next file, we stop processing. if not self._set_next_file(): self._pipe.set_state(self.Gst.State.NULL) self._main_loop.quit() @@ -299,6 +327,7 @@ class GStreamerBackend(object): self._pipe.set_state(self.Gst.State.NULL) self._main_loop.quit() err, debug = message.parse_error() + # A GStreamer error, either an unsupported format or a bug. raise Exception("Error %s - %s on file %s" % (err, debug, self._src.get_property("location"))) @@ -306,6 +335,8 @@ class GStreamerBackend(object): tags = message.parse_tag() def handle_tag(taglist, tag, userdata): + # The rganalysis element provides both the existing tags for files and the new computes tags. + # In order to ensure we store the computed tags, we overwrite the RG values of received a second time. if tag == self.Gst.TAG_TRACK_GAIN: self._file_tags[self._file]["TRACK_GAIN"] = \ taglist.get_double(tag)[1] @@ -335,31 +366,48 @@ class GStreamerBackend(object): return True + """ + Initialize the filesrc element with the next file to be analyzed. + """ def _set_file(self): + # No more files, we're done if len(self._files) == 0: return False self._file = self._files.pop(0) + # Disconnect the decodebin element from the pipeline, set its state to READY to to clear it. self._decbin.unlink(self._conv) self._decbin.set_state(self.Gst.State.READY) + # Set a new file on the filesrc element, can only be done in the READY state self._src.set_state(self.Gst.State.READY) self._src.set_property("location", syspath(self._file.path)) + # Ensure the filesrc element received the paused state of the pipeline in a blocking manner self._src.sync_state_with_parent() self._src.get_state(self.Gst.CLOCK_TIME_NONE) + + # Ensure the decodebin element receives the paused state of the pipeline in a blocking manner self._decbin.sync_state_with_parent() self._decbin.get_state(self.Gst.CLOCK_TIME_NONE) return True + """ + Set the next file to be analyzed while keeping the pipeline in the PAUSED state + so that the rganalysis element can correctly handle album gain + """ def _set_next_file(self): + # A blocking pause self._pipe.set_state(self.Gst.State.PAUSED) self._pipe.get_state(self.Gst.CLOCK_TIME_NONE) + # Try setting the next file ret = self._set_file() if ret: + # Seek to the beginning in order to clear the EOS state of the + # various elements of the pipeline self._pipe.seek_simple(self.Gst.Format.TIME, self.Gst.SeekFlags.FLUSH, 0) @@ -370,13 +418,16 @@ class GStreamerBackend(object): def _on_pad_added(self, decbin, pad): sink_pad = self._conv.get_compatible_pad(pad, None) if sink_pad is None: + # TODO: fatal, how should this be handled? raise Exception() pad.link(sink_pad) def _on_pad_removed(self, decbin, pad): + # Called when the decodebin element is disconnected from the rest of the pipeline while switching input files peer = pad.get_peer() if peer is not None: + # TODO: fatal, how should this be handled? raise Exception() @@ -394,25 +445,26 @@ class ReplayGainPlugin(BeetsPlugin): super(ReplayGainPlugin, self).__init__() self.import_stages = [self.imported] + # default backend is 'command' for backward-compatibility. self.config.add({ 'overwrite': False, 'auto': True, - 'backend': u'', + 'backend': u'command', }) self.overwrite = self.config['overwrite'].get(bool) self.automatic = self.config['auto'].get(bool) backend_name = self.config['backend'].get(unicode) - if backend_name not in ReplayGainPlugin.BACKENDS: + if backend_name not in BACKENDS: raise ui.UserError( u"Selected ReplayGain backend {0} is not supported. " u"Please select one of: {1}".format( backend_name, - u', '.join(ReplayGainPlugin.BACKENDS.keys()) + u', '.join(BACKENDS.keys()) ) ) - self.backend_instance = ReplayGainPlugin.BACKENDS[backend_name]( + self.backend_instance = BACKENDS[backend_name]( self.config ) From 6aa1cc95cb3266e3ce5af5aaca1ee964deedafd3 Mon Sep 17 00:00:00 2001 From: Yevgeny Bezman Date: Tue, 1 Apr 2014 23:09:38 +0300 Subject: [PATCH 08/26] replaygain: Improved error handling --- beetsplug/replaygain.py | 82 ++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 721d431ef..1add61bef 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -171,12 +171,8 @@ class CommandBackend(Backend): cmd = cmd + [syspath(i.path) for i in items] log.debug(u'replaygain: analyzing {0} files'.format(len(items))) - try: - log.debug(u"replaygain: executing %s" % " ".join(cmd)) - output = call(cmd) - except ReplayGainError as exc: - log.warn(u'replaygain: analysis failed ({0})'.format(exc)) - return + log.debug(u"replaygain: executing %s" % " ".join(cmd)) + output = call(cmd) log.debug(u'replaygain: analysis finished') results = self.parse_tool_output(output, len(items) + (1 if is_album else 0)) @@ -254,23 +250,22 @@ class GStreamerBackend(object): """Import the necessary GObject-related modules and assign `Gst` and `GObject` fields on this object. """ + print "here 1" import gi gi.require_version('Gst', '1.0') + print "here 1.5" from gi.repository import GObject, Gst # Thread initialization. The pipeline freezes if not initialized at this point. Not entirely sure why # this is not handled by the framwork. GObject.threads_init() Gst.init([sys.argv[0]]) + print "here 2" self.GObject = GObject self.Gst = Gst def compute(self, files, album): - if len(self._files) != 0: - # Previous invocation did not consume all files - raise Exception() - self._files = list(files) if len(self._files) == 0: @@ -287,8 +282,7 @@ class GStreamerBackend(object): def compute_track_gain(self, items): self.compute(items, False) if len(self._file_tags) != len(items): - # Some items did not recieve tags - raise Exception() + raise ReplayGainError("Some tracks did not receive tags") ret = [] for item in items: @@ -301,8 +295,7 @@ class GStreamerBackend(object): items = list(album.items()) self.compute(items, True) if len(self._file_tags) != len(items): - # Some items did not receive tags - raise Exception() + raise ReplayGainError("Some items in album did not receive tags") ret = [] for item in items: @@ -328,7 +321,7 @@ class GStreamerBackend(object): self._main_loop.quit() err, debug = message.parse_error() # A GStreamer error, either an unsupported format or a bug. - raise Exception("Error %s - %s on file %s" % + raise ReplayGainError("Error %s - %s on file %s" % (err, debug, self._src.get_property("location"))) def _on_tag(self, bus, message): @@ -360,6 +353,9 @@ class GStreamerBackend(object): return False self._file = self._files.pop(0) + + self._pipe.set_state(self.Gst.State.NULL) + self._src.set_property("location", syspath(self._file.path)) self._pipe.set_state(self.Gst.State.PLAYING) @@ -417,18 +413,13 @@ class GStreamerBackend(object): def _on_pad_added(self, decbin, pad): sink_pad = self._conv.get_compatible_pad(pad, None) - if sink_pad is None: - # TODO: fatal, how should this be handled? - raise Exception() - + assert(sink_pad is not None) pad.link(sink_pad) def _on_pad_removed(self, decbin, pad): # Called when the decodebin element is disconnected from the rest of the pipeline while switching input files peer = pad.get_peer() - if peer is not None: - # TODO: fatal, how should this be handled? - raise Exception() + assert(peer is None) # Main plugin logic. @@ -510,22 +501,24 @@ class ReplayGainPlugin(BeetsPlugin): log.info(u'analyzing {0} - {1}'.format(album.albumartist, album.album)) - album_gain = self.backend_instance.compute_album_gain(album) - if len(album_gain.track_gains) != len(album.items()): - log.warn( - u"ReplayGain backend failed " - u"for some tracks in album {0} - {1}".format( - album.albumartist, album.album + try: + album_gain = self.backend_instance.compute_album_gain(album) + if len(album_gain.track_gains) != len(album.items()): + raise ReplayGainError( + u"ReplayGain backend failed " + u"for some tracks in album {0} - {1}".format( + album.albumartist, album.album + ) ) - ) - return - self.store_album_gain(album, album_gain.album_gain) - for item, track_gain in itertools.izip(album.items(), - album_gain.track_gains): - self.store_track_gain(item, track_gain) - if write: - item.write() + self.store_album_gain(album, album_gain.album_gain) + for item, track_gain in itertools.izip(album.items(), + album_gain.track_gains): + self.store_track_gain(item, track_gain) + if write: + item.write() + except ReplayGainError, e: + log.warn(e) def handle_track(self, item, write): if not self.track_requires_gain(item): @@ -536,18 +529,21 @@ class ReplayGainPlugin(BeetsPlugin): log.info(u'analyzing {0} - {1}'.format(item.artist, item.title)) - track_gains = self.backend_instance.compute_track_gain([item]) - if len(track_gains) != 1: - log.warn( + try: + track_gains = self.backend_instance.compute_track_gain([item]) + if len(track_gains) != 1: + raise ReplayGainError( u"ReplayGain backend failed for track {0} - {1}".format( item.artist, item.title ) ) - return - self.store_track_gain(item, track_gains[0]) - if write: - item.write() + self.store_track_gain(item, track_gains[0]) + if write: + item.write() + except RepalyGainError, e: + log.warn(e) + def imported(self, session, task): """Our import stage function.""" From 89680d835a61ffbd56182f1e48eed9f74224c9c1 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 18:29:02 +0200 Subject: [PATCH 09/26] Refine docstrings and fix style issues (flake8) --- beetsplug/replaygain.py | 146 +++++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 61 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 1add61bef..2e99f734b 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -33,6 +33,7 @@ class ReplayGainError(Exception): """Raised when an error occurs during mp3gain/aacgain execution. """ + def call(args): """Execute the command and return its output or raise a ReplayGainError on failure. @@ -50,12 +51,12 @@ def call(args): raise ReplayGainError("argument encoding failed") - # Backend base and plumbing classes. Gain = collections.namedtuple("Gain", "gain peak") AlbumGain = collections.namedtuple("AlbumGain", "album_gain track_gains") + class Backend(object): """An abstract class representing engine for calculating RG values. """ @@ -110,18 +111,18 @@ class CommandBackend(Backend): target_level = config['targetlevel'].as_number() self.gain_offset = int(target_level - 89) - """ - Computes the track gain of the given tracks, returns a list of TrackGain objects - """ def compute_track_gain(self, items): + """Computes the track gain of the given tracks, returns a list + of TrackGain objects. + """ supported_items = filter(self.format_supported, items) output = self.compute_gain(supported_items, False) return output - """ - Computes the album gain of the given album, returns an AlbumGain object - """ def compute_album_gain(self, album): + """Computes the album gain of the given album, returns an + AlbumGain object. + """ # TODO: What should be done when not all tracks in the album are # supported? @@ -132,21 +133,22 @@ class CommandBackend(Backend): output = self.compute_gain(supported_items, True) return AlbumGain(output[-1], output[:-1]) - """ - Checks whether the given item is supported by the selected tool - """ def format_supported(self, item): + """Checks whether the given item is supported by the selected tool. + """ if 'mp3gain' in self.command and item.format != 'MP3': return False elif 'aacgain' in self.command and item.format not in ('MP3', 'AAC'): return False return True - """ - Computes the track or album gain of a list of items, returns a list of TrackGain objects - When computing album gain, the last TrackGain object returned is the album gain - """ def compute_gain(self, items, is_album): + """Computes the track or album gain of a list of items, returns + a list of TrackGain objects. + + When computing album gain, the last TrackGain object returned is + the album gain + """ if len(items) == 0: return [] @@ -206,17 +208,20 @@ class GStreamerBackend(object): def __init__(self, config): self._import_gst() - # Initialized a GStreamer pipeline of the form - # filesrc -> decodebin -> audioconvert -> audioresample -> rganalysis -> fakesink - # The connection between decodebin and audioconvert is handled dynamically after decodebin - # figures out the type of the input file. + # Initialized a GStreamer pipeline of the form filesrc -> + # decodebin -> audioconvert -> audioresample -> rganalysis -> + # fakesink The connection between decodebin and audioconvert is + # handled dynamically after decodebin figures out the type of + # the input file. self._src = self.Gst.ElementFactory.make("filesrc", "src") self._decbin = self.Gst.ElementFactory.make("decodebin", "decbin") self._conv = self.Gst.ElementFactory.make("audioconvert", "conv") self._res = self.Gst.ElementFactory.make("audioresample", "res") self._rg = self.Gst.ElementFactory.make("rganalysis", "rg") - # We check which files need gain ourselves, so all files given to rganalsys should have their gain - # computed, even if it already exists. + + # We check which files need gain ourselves, so all files given + # to rganalsys should have their gain computed, even if it + # already exists. self._rg.set_property("forced", True) self._sink = self.Gst.ElementFactory.make("fakesink", "sink") @@ -238,7 +243,8 @@ class GStreamerBackend(object): self._bus.connect("message::eos", self._on_eos) self._bus.connect("message::error", self._on_error) self._bus.connect("message::tag", self._on_tag) - # Needed for handling the dynamic connection between decodebin and audioconvert + # Needed for handling the dynamic connection between decodebin + # and audioconvert self._decbin.connect("pad-added", self._on_pad_added) self._decbin.connect("pad-removed", self._on_pad_removed) @@ -256,8 +262,9 @@ class GStreamerBackend(object): print "here 1.5" from gi.repository import GObject, Gst - # Thread initialization. The pipeline freezes if not initialized at this point. Not entirely sure why - # this is not handled by the framwork. + # Thread initialization. The pipeline freezes if not initialized + # at this point. Not entirely sure why this is not handled by + # the framwork. GObject.threads_init() Gst.init([sys.argv[0]]) print "here 2" @@ -310,8 +317,9 @@ class GStreamerBackend(object): self._bus.remove_signal_watch() def _on_eos(self, bus, message): - # A file finished playing in all elements of the pipeline. The RG tags have already been propagated. - # If we don't have a next file, we stop processing. + # A file finished playing in all elements of the pipeline. The + # RG tags have already been propagated. If we don't have a next + # file, we stop processing. if not self._set_next_file(): self._pipe.set_state(self.Gst.State.NULL) self._main_loop.quit() @@ -322,29 +330,31 @@ class GStreamerBackend(object): err, debug = message.parse_error() # A GStreamer error, either an unsupported format or a bug. raise ReplayGainError("Error %s - %s on file %s" % - (err, debug, self._src.get_property("location"))) + (err, debug, self._src.get_property("location"))) def _on_tag(self, bus, message): tags = message.parse_tag() def handle_tag(taglist, tag, userdata): - # The rganalysis element provides both the existing tags for files and the new computes tags. - # In order to ensure we store the computed tags, we overwrite the RG values of received a second time. + # The rganalysis element provides both the existing tags for + # files and the new computes tags. In order to ensure we + # store the computed tags, we overwrite the RG values of + # received a second time. if tag == self.Gst.TAG_TRACK_GAIN: self._file_tags[self._file]["TRACK_GAIN"] = \ - taglist.get_double(tag)[1] + taglist.get_double(tag)[1] elif tag == self.Gst.TAG_TRACK_PEAK: self._file_tags[self._file]["TRACK_PEAK"] = \ - taglist.get_double(tag)[1] + taglist.get_double(tag)[1] elif tag == self.Gst.TAG_ALBUM_GAIN: self._file_tags[self._file]["ALBUM_GAIN"] = \ - taglist.get_double(tag)[1] + taglist.get_double(tag)[1] elif tag == self.Gst.TAG_ALBUM_PEAK: self._file_tags[self._file]["ALBUM_PEAK"] = \ - taglist.get_double(tag)[1] + taglist.get_double(tag)[1] elif tag == self.Gst.TAG_REFERENCE_LEVEL: self._file_tags[self._file]["REFERENCE_LEVEL"] = \ - taglist.get_double(tag)[1] + taglist.get_double(tag)[1] tags.foreach(handle_tag, None) @@ -353,48 +363,47 @@ class GStreamerBackend(object): return False self._file = self._files.pop(0) - self._pipe.set_state(self.Gst.State.NULL) - self._src.set_property("location", syspath(self._file.path)) - self._pipe.set_state(self.Gst.State.PLAYING) - return True - """ - Initialize the filesrc element with the next file to be analyzed. - """ def _set_file(self): + """Initialize the filesrc element with the next file to be analyzed. + """ # No more files, we're done if len(self._files) == 0: return False self._file = self._files.pop(0) - # Disconnect the decodebin element from the pipeline, set its state to READY to to clear it. + # Disconnect the decodebin element from the pipeline, set its + # state to READY to to clear it. self._decbin.unlink(self._conv) self._decbin.set_state(self.Gst.State.READY) - # Set a new file on the filesrc element, can only be done in the READY state + # Set a new file on the filesrc element, can only be done in the + # READY state self._src.set_state(self.Gst.State.READY) self._src.set_property("location", syspath(self._file.path)) - # Ensure the filesrc element received the paused state of the pipeline in a blocking manner + # Ensure the filesrc element received the paused state of the + # pipeline in a blocking manner self._src.sync_state_with_parent() self._src.get_state(self.Gst.CLOCK_TIME_NONE) - # Ensure the decodebin element receives the paused state of the pipeline in a blocking manner + # Ensure the decodebin element receives the paused state of the + # pipeline in a blocking manner self._decbin.sync_state_with_parent() self._decbin.get_state(self.Gst.CLOCK_TIME_NONE) return True - """ - Set the next file to be analyzed while keeping the pipeline in the PAUSED state - so that the rganalysis element can correctly handle album gain - """ def _set_next_file(self): + """Set the next file to be analyzed while keeping the pipeline + in the PAUSED state so that the rganalysis element can correctly + handle album gain. + """ # A blocking pause self._pipe.set_state(self.Gst.State.PAUSED) self._pipe.get_state(self.Gst.CLOCK_TIME_NONE) @@ -417,7 +426,8 @@ class GStreamerBackend(object): pad.link(sink_pad) def _on_pad_removed(self, decbin, pad): - # Called when the decodebin element is disconnected from the rest of the pipeline while switching input files + # Called when the decodebin element is disconnected from the + # rest of the pipeline while switching input files peer = pad.get_peer() assert(peer is None) @@ -429,6 +439,7 @@ BACKENDS = { "gstreamer": GStreamerBackend, } + class ReplayGainPlugin(BeetsPlugin): """Provides ReplayGain analysis. """ @@ -461,7 +472,7 @@ class ReplayGainPlugin(BeetsPlugin): def track_requires_gain(self, item): return self.overwrite or \ - (not item.rg_track_gain or not item.rg_track_peak) + (not item.rg_track_gain or not item.rg_track_peak) def album_requires_gain(self, album): # Skip calculating gain only when *all* files don't need @@ -469,8 +480,8 @@ class ReplayGainPlugin(BeetsPlugin): # needs recalculation, we still get an accurate album gain # value. return self.overwrite or \ - any([not item.rg_album_gain or not item.rg_album_peak - for item in album.items()]) + any([not item.rg_album_gain or not item.rg_album_peak + for item in album.items()]) def store_track_gain(self, item, track_gain): item.rg_track_gain = track_gain.gain @@ -489,10 +500,16 @@ class ReplayGainPlugin(BeetsPlugin): log.debug(u'replaygain: applied album gain {0}, peak {1}'.format( album.rg_album_gain, - album.rg_album_peak - )) + album.rg_album_peak)) def handle_album(self, album, write): + """Compute album and track replay gain store it in all of the + album's items. + + If ``write`` is truthy then ``item.write()`` is called for each + item. If replay gain information is already present in all + items, nothing is done. + """ if not self.album_requires_gain(album): log.info(u'Skipping album {0} - {1}'.format(album.albumartist, album.album)) @@ -521,6 +538,12 @@ class ReplayGainPlugin(BeetsPlugin): log.warn(e) def handle_track(self, item, write): + """Compute track replay gain and store it in the item. + + If ``write`` is truthy then ``item.write()`` is called to write + the data to disk. If replay gain information is already present + in the item, nothing is done. + """ if not self.track_requires_gain(item): log.info(u'Skipping track {0} - {1}'.format(item.artist, item.title)) @@ -533,20 +556,20 @@ class ReplayGainPlugin(BeetsPlugin): track_gains = self.backend_instance.compute_track_gain([item]) if len(track_gains) != 1: raise ReplayGainError( - u"ReplayGain backend failed for track {0} - {1}".format( - item.artist, item.title + u"ReplayGain backend failed for track {0} - {1}".format( + item.artist, item.title + ) ) - ) self.store_track_gain(item, track_gains[0]) if write: item.write() - except RepalyGainError, e: + except ReplayGainError, e: log.warn(e) - def imported(self, session, task): - """Our import stage function.""" + """Add replay gain info to items or albums of ``task``. + """ if not self.automatic: return @@ -557,7 +580,8 @@ class ReplayGainPlugin(BeetsPlugin): self.handle_track(task.item, False) def commands(self): - """Provide a ReplayGain command.""" + """Return the "replaygain" ui subcommand. + """ def func(lib, opts, args): write = config['import']['write'].get(bool) From 439fc1938f4e315f7153f658d87bf3462e92409c Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 18:34:59 +0200 Subject: [PATCH 10/26] Remove debug print statements --- beetsplug/replaygain.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 2e99f734b..a57cd97ed 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -256,10 +256,8 @@ class GStreamerBackend(object): """Import the necessary GObject-related modules and assign `Gst` and `GObject` fields on this object. """ - print "here 1" import gi gi.require_version('Gst', '1.0') - print "here 1.5" from gi.repository import GObject, Gst # Thread initialization. The pipeline freezes if not initialized @@ -267,7 +265,6 @@ class GStreamerBackend(object): # the framwork. GObject.threads_init() Gst.init([sys.argv[0]]) - print "here 2" self.GObject = GObject self.Gst = Gst From a5bdbdcf7fbb9bec510cc41aea8753f83e4d622d Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 18:42:26 +0200 Subject: [PATCH 11/26] Move available backends to class level See also https://github.com/sampsyo/beets/issues/650 --- beetsplug/replaygain.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index a57cd97ed..2cb022b95 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -431,15 +431,15 @@ class GStreamerBackend(object): # Main plugin logic. -BACKENDS = { - "command": CommandBackend, - "gstreamer": GStreamerBackend, -} - - class ReplayGainPlugin(BeetsPlugin): """Provides ReplayGain analysis. """ + + backends = { + "command": CommandBackend, + "gstreamer": GStreamerBackend, + } + def __init__(self): super(ReplayGainPlugin, self).__init__() self.import_stages = [self.imported] @@ -454,16 +454,16 @@ class ReplayGainPlugin(BeetsPlugin): self.overwrite = self.config['overwrite'].get(bool) self.automatic = self.config['auto'].get(bool) backend_name = self.config['backend'].get(unicode) - if backend_name not in BACKENDS: + if backend_name not in self.backends: raise ui.UserError( u"Selected ReplayGain backend {0} is not supported. " u"Please select one of: {1}".format( backend_name, - u', '.join(BACKENDS.keys()) + u', '.join(self.backends.keys()) ) ) - self.backend_instance = BACKENDS[backend_name]( + self.backend_instance = self.backends[backend_name]( self.config ) From d8c37d6ca3d520a2c7dd7a8fd63c7909744d5ef6 Mon Sep 17 00:00:00 2001 From: Yevgeny Bezman Date: Fri, 4 Apr 2014 20:12:23 +0300 Subject: [PATCH 12/26] replaygain: Added a FatalReplayGainError class to signal the plugin that the backend failed completely --- beetsplug/replaygain.py | 57 ++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 2cb022b95..aeb4b1c9c 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -30,7 +30,11 @@ log = logging.getLogger('beets') # Utilities. class ReplayGainError(Exception): - """Raised when an error occurs during mp3gain/aacgain execution. + """Raised when a local (to a track or an album) error occurs in one of the backends. + """ + +class FatalReplayGainError(Exception): + """Raised when a fatal error occurs in one of the backends. """ @@ -89,7 +93,7 @@ class CommandBackend(Backend): if self.command: # Explicit executable path. if not os.path.isfile(self.command): - raise ui.UserError( + raise FatalReplayGainError( 'replaygain command does not exist: {0}'.format( self.command ) @@ -103,7 +107,7 @@ class CommandBackend(Backend): except OSError: pass if not self.command: - raise ui.UserError( + raise FatalReplayGainError( 'no replaygain command found: install mp3gain or aacgain' ) @@ -256,15 +260,19 @@ class GStreamerBackend(object): """Import the necessary GObject-related modules and assign `Gst` and `GObject` fields on this object. """ - import gi - gi.require_version('Gst', '1.0') - from gi.repository import GObject, Gst - # Thread initialization. The pipeline freezes if not initialized - # at this point. Not entirely sure why this is not handled by - # the framwork. - GObject.threads_init() - Gst.init([sys.argv[0]]) + try: + import gi + gi.require_version('Gst', '1.0') + + from gi.repository import GObject, Gst + # Thread initialization. The pipeline freezes if not initialized + # at this point. Not entirely sure why this is not handled by + # the framwork. + GObject.threads_init() + Gst.init([sys.argv[0]]) + except: + raise FatalReplayGainError("GStreamer failed to initialize") self.GObject = GObject self.Gst = Gst @@ -463,9 +471,14 @@ class ReplayGainPlugin(BeetsPlugin): ) ) - self.backend_instance = self.backends[backend_name]( - self.config - ) + try: + self.backend_instance = self.backends[backend_name]( + self.config + ) + except (ReplayGainError, FatalReplayGainError) as e: + raise ui.UserError( + 'An error occured in backend initialization: {0}'.format(e) + ) def track_requires_gain(self, item): return self.overwrite or \ @@ -531,8 +544,12 @@ class ReplayGainPlugin(BeetsPlugin): self.store_track_gain(item, track_gain) if write: item.write() - except ReplayGainError, e: - log.warn(e) + except ReplayGainError as e: + log.warn(u"ReplayGain error: {1}".format(e)) + except FatalReplayGainError as e: + raise ui.UserError( + u"Fatal replay gain error: {1}".format(e) + ) def handle_track(self, item, write): """Compute track replay gain and store it in the item. @@ -561,8 +578,12 @@ class ReplayGainPlugin(BeetsPlugin): self.store_track_gain(item, track_gains[0]) if write: item.write() - except ReplayGainError, e: - log.warn(e) + except ReplayGainError as e: + log.warn(u"ReplayGain error: {1}".format(e)) + except FatalReplayGainError as e: + raise ui.UserError( + u"Fatal replay gain error: {1}".format(e) + ) def imported(self, session, task): """Add replay gain info to items or albums of ``task``. From cf49d88156ed67220e71a2f04b501118941e623a Mon Sep 17 00:00:00 2001 From: Yevgeny Bezman Date: Fri, 4 Apr 2014 20:20:03 +0300 Subject: [PATCH 13/26] replaygain: clarified the need to strange thread initialization| --- beetsplug/replaygain.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index aeb4b1c9c..4a6a31c19 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -266,9 +266,8 @@ class GStreamerBackend(object): gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst - # Thread initialization. The pipeline freezes if not initialized - # at this point. Not entirely sure why this is not handled by - # the framwork. + # Calling GObject.threads_init() is not needed for + # PyGObject 3.10.2+ GObject.threads_init() Gst.init([sys.argv[0]]) except: From 81f53fb0d29a8fe61188de875a79a36dfef6e7ce Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 19:44:16 +0200 Subject: [PATCH 14/26] Add basic cli tests for replaygain --- beetsplug/replaygain.py | 4 +- test/test_replaygain.py | 133 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 test/test_replaygain.py diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 4a6a31c19..354f5b991 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -30,9 +30,11 @@ log = logging.getLogger('beets') # Utilities. class ReplayGainError(Exception): - """Raised when a local (to a track or an album) error occurs in one of the backends. + """Raised when a local (to a track or an album) error occurs in one + of the backends. """ + class FatalReplayGainError(Exception): """Raised when a fatal error occurs in one of the backends. """ diff --git a/test/test_replaygain.py b/test/test_replaygain.py new file mode 100644 index 000000000..3d1afe8d3 --- /dev/null +++ b/test/test_replaygain.py @@ -0,0 +1,133 @@ +# This file is part of beets. +# Copyright 2013, Thomas Scholtes +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + + +import os +import tempfile +import shutil +from glob import glob + +import _common +from _common import unittest + +import beets +from beets import ui +from beets.library import Item, Album +from beets.mediafile import MediaFile + + +class ReplayGainGstCliTest(unittest.TestCase): + + def setUp(self): + self.setupBeets() + self.config['replaygain']['backend'] = u'gstreamer' + self.config['plugins'] = ['replaygain'] + self.setupLibrary(2) + + def tearDown(self): + del os.environ['BEETSDIR'] + shutil.rmtree(self.temp_dir) + + def setupBeets(self): + self.temp_dir = tempfile.mkdtemp() + os.environ['BEETSDIR'] = self.temp_dir + + self.config = beets.config + self.config.clear() + self.config.read(user=False) + + self.config['verbose'] = True + self.config['color'] = False + self.config['threaded'] = False + self.config['import']['copy'] = False + + self.libdir = os.path.join(self.temp_dir, 'libdir') + os.mkdir(self.libdir) + self.config['directory'] = self.libdir + + self.libpath = os.path.join(self.temp_dir, 'lib') + self.config['library'] = self.libpath + + self.lib = beets.library.Library(self.config['library'].as_filename(), + self.libdir) + + def setupLibrary(self, file_count): + """Add an album to the library with ``file_count`` items. + """ + album = Album(id=1) + album.add(self.lib) + + fixture_glob = os.path.join(_common.RSRC, '*.mp3') + for src in glob(fixture_glob)[0:file_count]: + dst = os.path.join(self.libdir, os.path.basename(src)) + shutil.copy(src, dst) + item = Item.from_path(dst) + item.album_id = 1 + item.add(self.lib) + self._reset_replaygain(item) + + def _reset_replaygain(self, item): + item['rg_track_peak'] = 0 + item['rg_track_gain'] = 0 + item['rg_album_gain'] = 0 + item['rg_album_gain'] = 0 + item.write() + item.store() + + def test_cli_saves_track_gain(self): + for item in self.lib.items(): + self.assertEqual(item.rg_track_peak, 0.0) + self.assertEqual(item.rg_track_gain, 0.0) + mediafile = MediaFile(item.path) + self.assertEqual(mediafile.rg_track_peak, 0.0) + self.assertEqual(mediafile.rg_track_gain, 0.0) + + ui._raw_main(['replaygain']) + for item in self.lib.items(): + self.assertNotEqual(item.rg_track_peak, 0.0) + self.assertNotEqual(item.rg_track_gain, 0.0) + mediafile = MediaFile(item.path) + self.assertAlmostEqual( + mediafile.rg_track_peak, item.rg_track_peak, places=6) + self.assertAlmostEqual( + mediafile.rg_track_gain, item.rg_track_gain, places=6) + + def test_cli_saves_album_gain_to_file(self): + for item in self.lib.items(): + mediafile = MediaFile(item.path) + self.assertEqual(mediafile.rg_album_peak, 0.0) + self.assertEqual(mediafile.rg_album_gain, 0.0) + + ui._raw_main(['replaygain', '-a']) + + peaks = [] + gains = [] + for item in self.lib.items(): + mediafile = MediaFile(item.path) + peaks.append(mediafile.rg_album_peak) + gains.append(mediafile.rg_album_gain) + + # Make sure they are all the same + self.assertEqual(max(peaks), min(peaks)) + self.assertEqual(max(gains), min(gains)) + + self.assertNotEqual(max(gains), 0.0) + self.assertNotEqual(max(peaks), 0.0) + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + +if __name__ == '__main__': + unittest.main(defaultTest='suite') From 81a2433e9dbfd4a279a15c4671ba554bd407cba1 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 19:46:43 +0200 Subject: [PATCH 15/26] Use GLib.MainLoop instead of deprecated GObject --- beetsplug/replaygain.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 354f5b991..228db080f 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -254,7 +254,7 @@ class GStreamerBackend(object): self._decbin.connect("pad-added", self._on_pad_added) self._decbin.connect("pad-removed", self._on_pad_removed) - self._main_loop = self.GObject.MainLoop() + self._main_loop = self.GLib.MainLoop() self._files = [] @@ -267,7 +267,7 @@ class GStreamerBackend(object): import gi gi.require_version('Gst', '1.0') - from gi.repository import GObject, Gst + from gi.repository import GObject, Gst, GLib # Calling GObject.threads_init() is not needed for # PyGObject 3.10.2+ GObject.threads_init() @@ -276,6 +276,7 @@ class GStreamerBackend(object): raise FatalReplayGainError("GStreamer failed to initialize") self.GObject = GObject + self.GLib = GLib self.Gst = Gst def compute(self, files, album): From 5d666fa4e74c1f5947f08f272b90a877e6b82f1b Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 19:52:39 +0200 Subject: [PATCH 16/26] Reset config and plugins after tests --- test/test_replaygain.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test_replaygain.py b/test/test_replaygain.py index 3d1afe8d3..588fc4b4d 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -23,6 +23,7 @@ from _common import unittest import beets from beets import ui +from beets import plugins from beets.library import Item, Album from beets.mediafile import MediaFile @@ -38,6 +39,10 @@ class ReplayGainGstCliTest(unittest.TestCase): def tearDown(self): del os.environ['BEETSDIR'] shutil.rmtree(self.temp_dir) + self.config.clear() + self.config.read(user=False) + plugins._classes = set() + plugins._instances = {} def setupBeets(self): self.temp_dir = tempfile.mkdtemp() From 43f2c483b7aa95e9f15b49217bf7d93648ad10dd Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 19:58:34 +0200 Subject: [PATCH 17/26] Install gstreamer1.0 on travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index fbef76f77..f9a486526 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,10 @@ matrix: - env: TOX_ENV=flake8 install: + - travis_retry sudo add-apt-repository -y ppa:gstreamer-developers/ppa - travis_retry sudo apt-get update - travis_retry sudo apt-get install -qq bash-completion + - travis_retry sudo apt-get install -qq gstreamer1.0-plugins-good - travis_retry pip install tox sphinx - "[[ $TOX_ENV == 'py27' ]] && pip install coveralls || true" From c54d8cb96c76c58a955b2b47f21ddca062e69d52 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 20:09:44 +0200 Subject: [PATCH 18/26] Add python-gi and bad gstreamer plugins to travis --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f9a486526..b3db30c2d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,8 +19,7 @@ matrix: install: - travis_retry sudo add-apt-repository -y ppa:gstreamer-developers/ppa - travis_retry sudo apt-get update - - travis_retry sudo apt-get install -qq bash-completion - - travis_retry sudo apt-get install -qq gstreamer1.0-plugins-good + - travis_retry sudo apt-get install -qq bash-completion python-gi gstreamer1.0-plugins-good gstramer1.0-plugins-bad - travis_retry pip install tox sphinx - "[[ $TOX_ENV == 'py27' ]] && pip install coveralls || true" From 5d40dc0fab0d179c6d149792ce470248cfed900e Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 20:16:03 +0200 Subject: [PATCH 19/26] Fix typo in .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b3db30c2d..6c862d559 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ matrix: install: - travis_retry sudo add-apt-repository -y ppa:gstreamer-developers/ppa - travis_retry sudo apt-get update - - travis_retry sudo apt-get install -qq bash-completion python-gi gstreamer1.0-plugins-good gstramer1.0-plugins-bad + - travis_retry sudo apt-get install -qq bash-completion python-gi gstreamer1.0-plugins-good gstreamer1.0-plugins-bad - travis_retry pip install tox sphinx - "[[ $TOX_ENV == 'py27' ]] && pip install coveralls || true" From 04ab2be424bc3b43487ff7f394e24da84cd37cad Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 20:39:13 +0200 Subject: [PATCH 20/26] Final attempt at travis and gstreamer --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6c862d559..ffc184e12 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,10 @@ matrix: install: - travis_retry sudo add-apt-repository -y ppa:gstreamer-developers/ppa - travis_retry sudo apt-get update - - travis_retry sudo apt-get install -qq bash-completion python-gi gstreamer1.0-plugins-good gstreamer1.0-plugins-bad + - > + travis_retry sudo apt-get install -qq + bash-completion python-gi gir1.2-gstreamer-1.0 + gstreamer1.0-plugins-good gstreamer1.0-plugins-bad - travis_retry pip install tox sphinx - "[[ $TOX_ENV == 'py27' ]] && pip install coveralls || true" From b47e6dd4d49f4fff1c988c88b73d135fbec901f8 Mon Sep 17 00:00:00 2001 From: Yevgeny Bezman Date: Fri, 4 Apr 2014 22:01:30 +0300 Subject: [PATCH 21/26] replaygain: added a test for skipping already calculated items --- test/test_replaygain.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/test_replaygain.py b/test/test_replaygain.py index 588fc4b4d..4b9d64263 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -108,6 +108,17 @@ class ReplayGainGstCliTest(unittest.TestCase): self.assertAlmostEqual( mediafile.rg_track_gain, item.rg_track_gain, places=6) + def test_cli_skips_calculated_tracks(self): + ui._raw_main(['replaygain']) + item = self.lib.items()[0] + peak = item.rg_track_peak + item.rg_track_gain = 0.0 + ui._raw_main(['replaygain']) + self.assertEqual(item.rg_track_gain, 0.0) + self.assertEqual(item.rg_track_peak, peak) + + + def test_cli_saves_album_gain_to_file(self): for item in self.lib.items(): mediafile = MediaFile(item.path) From 1c598d4cee30cc9f18bdbed9f92c74876ea8ca97 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 20:52:37 +0200 Subject: [PATCH 22/26] Add replaygain target level for gstreamer --- beetsplug/replaygain.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 228db080f..9fa894177 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -87,7 +87,6 @@ class CommandBackend(Backend): config.add({ 'command': u"", 'noclip': True, - 'targetlevel': 89, }) self.command = config["command"].get(unicode) @@ -229,6 +228,8 @@ class GStreamerBackend(object): # to rganalsys should have their gain computed, even if it # already exists. self._rg.set_property("forced", True) + self._rg.set_property("reference-level", + config["targetlevel"].as_number()) self._sink = self.Gst.ElementFactory.make("fakesink", "sink") self._pipe = self.Gst.Pipeline() @@ -459,6 +460,7 @@ class ReplayGainPlugin(BeetsPlugin): 'overwrite': False, 'auto': True, 'backend': u'command', + 'targetlevel': 89, }) self.overwrite = self.config['overwrite'].get(bool) From 5b277eedf83322f5bca477b84def21b1bb571d3b Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 21:11:12 +0200 Subject: [PATCH 23/26] Add replaygain test for command backend --- .travis.yml | 1 + test/test_replaygain.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ffc184e12..809f81188 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,7 @@ install: travis_retry sudo apt-get install -qq bash-completion python-gi gir1.2-gstreamer-1.0 gstreamer1.0-plugins-good gstreamer1.0-plugins-bad + mp3gain - travis_retry pip install tox sphinx - "[[ $TOX_ENV == 'py27' ]] && pip install coveralls || true" diff --git a/test/test_replaygain.py b/test/test_replaygain.py index 4b9d64263..76ec480c8 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -28,11 +28,11 @@ from beets.library import Item, Album from beets.mediafile import MediaFile -class ReplayGainGstCliTest(unittest.TestCase): +class ReplayGainCliTestBase(object): def setUp(self): self.setupBeets() - self.config['replaygain']['backend'] = u'gstreamer' + self.config['replaygain']['backend'] = self.backend self.config['plugins'] = ['replaygain'] self.setupLibrary(2) @@ -142,6 +142,14 @@ class ReplayGainGstCliTest(unittest.TestCase): self.assertNotEqual(max(peaks), 0.0) +class ReplayGainGstCliTest(ReplayGainCliTestBase, unittest.TestCase): + backend = u'gstreamer' + + +class ReplayGainCmdCliTest(ReplayGainCliTestBase, unittest.TestCase): + backend = u'command' + + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From 6286bc0b0f8ab39529228fbe7c386efb352969fa Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 22:38:48 +0200 Subject: [PATCH 24/26] Include site-packages on travis and skip tests otherwise --- .travis.yml | 3 ++- test/test_replaygain.py | 10 ++++++++-- tox.ini | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 809f81188..939301854 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: python -python: 2.7 +virtualenv: + system_site_packages: true branches: only: diff --git a/test/test_replaygain.py b/test/test_replaygain.py index 76ec480c8..39a765465 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -27,6 +27,13 @@ from beets import plugins from beets.library import Item, Album from beets.mediafile import MediaFile +try: + import gi + gi.require_version('Gst', '1.0') + GST_AVAILABLE = True +except ImportError, ValueError: + GST_AVAILABLE = False + class ReplayGainCliTestBase(object): @@ -117,8 +124,6 @@ class ReplayGainCliTestBase(object): self.assertEqual(item.rg_track_gain, 0.0) self.assertEqual(item.rg_track_peak, peak) - - def test_cli_saves_album_gain_to_file(self): for item in self.lib.items(): mediafile = MediaFile(item.path) @@ -142,6 +147,7 @@ class ReplayGainCliTestBase(object): self.assertNotEqual(max(peaks), 0.0) +@unittest.skipIf(not GST_AVAILABLE, 'gstreamer cannot be found') class ReplayGainGstCliTest(ReplayGainCliTestBase, unittest.TestCase): backend = u'gstreamer' diff --git a/tox.ini b/tox.ini index 3aef31627..ba1ab45ef 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ deps = responses commands = nosetests +sitepackages = True [testenv:py26] deps = From c77b030f15f7894c9a00e1280370f3af726af70b Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 22:59:44 +0200 Subject: [PATCH 25/26] Travis and gstreamer. One last try. Seriously. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 939301854..0bc92444a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ install: travis_retry sudo apt-get install -qq bash-completion python-gi gir1.2-gstreamer-1.0 gstreamer1.0-plugins-good gstreamer1.0-plugins-bad - mp3gain + gstreamer1.0-plugins-ugly mp3gain - travis_retry pip install tox sphinx - "[[ $TOX_ENV == 'py27' ]] && pip install coveralls || true" From 4f844dfb925efd6fcc7ca9e6df09da627b2704fc Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 4 Apr 2014 23:15:39 +0200 Subject: [PATCH 26/26] Travis and gstreamer: I give up! --- .travis.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0bc92444a..70b4e8186 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,4 @@ language: python -virtualenv: - system_site_packages: true branches: only: @@ -18,13 +16,8 @@ matrix: - env: TOX_ENV=flake8 install: - - travis_retry sudo add-apt-repository -y ppa:gstreamer-developers/ppa - travis_retry sudo apt-get update - - > - travis_retry sudo apt-get install -qq - bash-completion python-gi gir1.2-gstreamer-1.0 - gstreamer1.0-plugins-good gstreamer1.0-plugins-bad - gstreamer1.0-plugins-ugly mp3gain + - travis_retry sudo apt-get install -qq bash-completion mp3gain - travis_retry pip install tox sphinx - "[[ $TOX_ENV == 'py27' ]] && pip install coveralls || true"