Repository: beetbox/beets

Issue:
replaygain: Handle invalid XML output from bs1770gain
### Problem

Beets crashes when calculating ReplayGain. I initially tought it was the import function (#2982), but it's indeed the RG plugin.

Running this command in verbose (`-vv`) mode:

```sh
$ beet -vv replaygain
```

Led to this problem:

```
[…]
replaygain: executing bs1770gain --ebu --xml -p /home/breversa/Musique/00tz 00tz/Endzeit Bunkertracks [act VII] – The bonus tracks/00tz 00tz - Poisoned minds and broken hearts (Alarm mix).flac /home/breversa/Musique/[Fabrikmutter]/Endzeit Bunkertracks [act VII] – The bonus tracks/[Fabrikmutter] - The minimal devotion.flac /home/breversa/Musique/[Product]/Endzeit Bunkertracks [act VII] – The bonus tracks/[Product] - To the wind (chant).flac /home/breversa/Musique/[Synaptic_Reactor]/Endzeit Bunkertracks [act VII] – The bonus tracks/[Synaptic_Reactor] - The devil's work (Ruinizer mix).flac /home/breversa/Musique/A.D.R.O.N./Endzeit Bunkertracks [act VII] – The bonus tracks/A.D.R.O.N. - Be subordinated.flac /home/breversa/Musique/Aim & Execute/Endzeit Bunkertracks [act VII] – The bonus tracks/Aim & Execute - Phantom energy.flac /home/breversa/Musique/Antibody/Endzeit Bunkertracks [act VII] – The bonus tracks/Antibody - Shrimps.flac /home/breversa/Musique/Ayria/Endzeit Bunkertracks [act VII] – The bonus tracks/Ayria - Hunger (Acylum mix).flac /home/breversa/Musique/Bereshit/Endzeit Bunkertracks [act VII] – The bonus tracks/Bereshit - Virtual freedom.flac /home/breversa/Musique/Biomechanimal/Endzeit Bunkertracks [act VII] – The bonus tracks/Biomechanimal - Elder gods.flac /home/breversa/Musique/C-[Phalea]/Endzeit Bunkertracks [act VII] – The bonus tracks/C-[Phalea] - Process to filth.flac /home/breversa/Musique/C2/Endzeit Bunkertracks [act VII] – The bonus tracks/C2 - Affirmative negative infinite.flac /home/breversa/Musique/Code_ Red Core/Endzeit Bunkertracks [act VII] – The bonus tracks/Code_ Red Core - Miss betrayal.flac /home/breversa/Musique/Denial of Service/Endzeit Bunkertracks [act VII] – The bonus tracks/Denial of Service - Red cross red crescent.flac /home/breversa/Musique/Die System/Endzeit Bunkertracks [act VII] – The bonus tracks/Die System - Cyanide.flac /home/breversa/Musique/Diverje/Endzeit Bunkertracks [act VII] – The bonus tracks/Diverje - All the fakes (touched by Stahlnebel & Black Selket).flac /home/breversa/Musique/Dolls of Pain/Endzeit Bunkertracks [act VII] – The bonus tracks/Dolls of Pain - Drugs on the floor (Endzeit mix).flac /home/breversa/Musique/Durandal x Xiescive/Endzeit Bunkertracks [act VII] – The bonus tracks/Durandal x Xiescive - Rabid.flac /home/breversa/Musique/DYM/Endzeit Bunkertracks [act VII] – The bonus tracks/DYM - With a smile (edit).flac /home/breversa/Musique/Flammpunkt/Endzeit Bunkertracks [act VII] – The bonus tracks/Flammpunkt - One in the brain (Binary Division mix).flac /home/breversa/Musique/Framework/Endzeit Bunkertracks [act VII] – The bonus tracks/Framework - Faith (Contact #2).flac /home/breversa/Musique/Ginger Snap5/Endzeit Bunkertracks [act VII] – The bonus tracks/Ginger Snap5 - Waiting for… (Pride and Fall mix).flac /home/breversa/Musique/Gusano/Endzeit Bunkertracks [act VII] – The bonus tracks/Gusano - Sangre eternal.flac /home/breversa/Musique/Hasswut/Endzeit Bunkertracks [act VII] – The bonus tracks/Hasswut - Nicht für mich.flac /home/breversa/Musique/Hydra Division V/Endzeit Bunkertracks [act VII] – The bonus tracks/Hydra Division V - Ostracized.flac /home/breversa/Musique/Larva/Endzeit Bunkertracks [act VII] – The bonus tracks/Larva - Mi mundo nunco fue el vuestro.flac /home/breversa/Musique/M.O.D./Endzeit Bunkertracks [act VII] – The bonus tracks/M.O.D. - Without regrets.flac /home/breversa/Musique/Mechanic Doll Machine/Endzeit Bunkertracks [act VII] – The bonus tracks/Mechanic Doll Machine - Mirage.flac /home/breversa/Musique/Mentallo and The Fixer/Endzeit Bunkertracks [act VII] – The bonus tracks/Mentallo and The Fixer - Gammera (Equinox).flac /home/breversa/Musique/Monospore/Endzeit Bunkertracks [act VII] – The bonus tracks/Monospore - Standing high.flac /home/breversa/Musique/MRDTC/Endzeit Bunkertracks [act VII] – The bonus tracks/MRDTC - Brain talk (Bunker hellectric).flac /home/breversa/Musique/Neonsol/Endzeit Bunkertracks [act VII] – The bonus tracks/Neonsol - Manipulation.flac /home/breversa/Musique/Neustrohm/Endzeit Bunkertracks [act VII] – The bonus tracks/Neustrohm - Non toxic.flac /home/breversa/Musique/Protectorate/Endzeit Bunkertracks [act VII] – The bonus tracks/Protectorate - Universal exports.flac /home/breversa/Musique/Psychicold/Endzeit Bunkertracks [act VII] – The bonus tracks/Psychicold - Burn in Hell.flac /home/breversa/Musique/Renoized/Endzeit Bunkertracks [act VII] – The bonus tracks/Renoized - Defcon.flac /home/breversa/Musique/Riotlegion/Endzeit Bunkertracks [act VII] – The bonus tracks/Riotlegion - God(b)less.flac /home/breversa/Musique/Sequenz_/Endzeit Bunkertracks [act VII] – The bonus tracks/Sequenz_ - Despair (A.D.N. mix).flac /home/breversa/Musique/Shadow Lady/Endzeit Bunkertracks [act VII] – The bonus tracks/Shadow Lady - Species (Endzeit mix).flac /home/breversa/Musique/Shadow System/Endzeit Bunkertracks [act VII] – The bonus tracks/Shadow System - Nex.flac /home/breversa/Musique/Statik Sky/Endzeit Bunkertracks [act VII] – The bonus tracks/Statik Sky - Fuqin noize.flac /home/breversa/Musique/Studio-X/Endzeit Bunkertracks [act VII] – The bonus tracks/Studio-X - The source of energy.flac /home/breversa/Musique/Suicidal Romance/Endzeit Bunkertracks [act VII] – The bonus tracks/Suicidal Romance - Touch (ES23 mix).flac /home/breversa/Musique/Switchface/Endzeit Bunkertracks [act VII] – The bonus tracks/Switchface - A crack in the monolith.flac /home/breversa/Musique/Synapsyche/Endzeit Bunkertracks [act VII] – The bonus tracks/Synapsyche - Breath control.flac /home/breversa/Musique/Tamtrum/Endzeit Bunkertracks [act VII] – The bonus tracks/Tamtrum - Le son de la pluie (Fils de pute mix by Grendel).flac /home/breversa/Musique/Third Realm/Endzeit Bunkertracks [act VII] – The bonus tracks/Third Realm - Deliverance.flac /home/breversa/Musique/Traumatize/Endzeit Bunkertracks [act VII] – The bonus tracks/Traumatize - Moje odbicie.flac /home/breversa/Musique/Unter Null/Endzeit Bunkertracks [act VII] – The bonus tracks/Unter Null - The fall (The Chemical Sweet Kid mix).flac /home/breversa/Musique/Vault 113/Endzeit Bunkertracks [act VII] – The bonus tracks/Vault 113 - The prophecy.flac
Sending event: album_imported
replaygain: analysis finished: <bs1770gain>
  <album>
    <track total="50" number="1" file="00tz 00tz - Poisoned minds and broken hearts (Alarm mix).flac">
      <integrated lufs="-7.16" lu="-15.84" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
    <track total="50" number="2" file="A.D.R.O.N. - Be subordinated.flac">
      <integrated lufs="-6.34" lu="-16.66" />
      <sample-peak spfs="-0.20" factor="0.977111" />
    </track>
    <track total="50" number="3" file="Aim & Execute - Phantom energy.flac">
      <integrated lufs="-7.90" lu="-15.10" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
    <track total="50" number="4" file="Antibody - Shrimps.flac">
      <integrated lufs="-5.91" lu="-17.09" />
      <sample-peak spfs="-0.20" factor="0.977111" />
    </track>
    <track total="50" number="5" file="Ayria - Hunger (Acylum mix).flac">
      <integrated lufs="-8.48" lu="-14.52" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
    <track total="50" number="6" file="Bereshit - Virtual freedom.flac">
      <integrated lufs="-5.49" lu="-17.51" />
      <sample-peak spfs="-0.21" factor="0.976043" />
    </track>
    <track total="50" number="7" file="Biomechanimal - Elder gods.flac">
      <integrated lufs="-8.85" lu="-14.15" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="8" file="C-[Phalea] - Process to filth.flac">
      <integrated lufs="-8.00" lu="-15.00" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="9" file="C2 - Affirmative negative infinite.flac">
      <integrated lufs="-8.91" lu="-14.09" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
    <track total="50" number="10" file="Code_ Red Core - Miss betrayal.flac">
      <integrated lufs="-7.46" lu="-15.54" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="11" file="DYM - With a smile (edit).flac">
      <integrated lufs="-9.42" lu="-13.58" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
    <track total="50" number="12" file="Denial of Service - Red cross red crescent.flac">
      <integrated lufs="-8.26" lu="-14.74" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
    <track total="50" number="13" file="Die System - Cyanide.flac">
      <integrated lufs="-8.53" lu="-14.47" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
    <track total="50" number="14" file="Diverje - All the fakes (touched by Stahlnebel & Black Selket).flac">
      <integrated lufs="-8.13" lu="-14.87" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="15" file="Dolls of Pain - Drugs on the floor (Endzeit mix).flac">
      <integrated lufs="-8.38" lu="-14.62" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="16" file="Durandal x Xiescive - Rabid.flac">
      <integrated lufs="-9.56" lu="-13.44" />
      <sample-peak spfs="0.00" factor="1.000031" />
    </track>
    <track total="50" number="17" file="Flammpunkt - One in the brain (Binary Division mix).flac">
      <integrated lufs="-9.36" lu="-13.64" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="18" file="Framework - Faith (Contact #2).flac">
      <integrated lufs="-8.65" lu="-14.35" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="19" file="Ginger Snap5 - Waiting for… (Pride and Fall mix).flac">
      <integrated lufs="-6.26" lu="-16.74" />
      <sample-peak spfs="-0.15" factor="0.982391" />
    </track>
    <track total="50" number="20" file="Gusano - Sangre eternal.flac">
      <integrated lufs="-7.86" lu="-15.14" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
    <track total="50" number="21" file="Hasswut - Nicht für mich.flac">
      <integrated lufs="-5.16" lu="-17.84" />
      <sample-peak spfs="-0.30" factor="0.965514" />
    </track>
    <track total="50" number="22" file="Hydra Division V - Ostracized.flac">
      <integrated lufs="-8.22" lu="-14.78" />
      <sample-peak spfs="-0.17" factor="0.980499" />
    </track>
    <track total="50" number="23" file="Larva - Mi mundo nunco fue el vuestro.flac">
      <integrated lufs="-9.14" lu="-13.86" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="24" file="M.O.D. - Without regrets.flac">
      <integrated lufs="-5.29" lu="-17.71" />
      <sample-peak spfs="-0.01" factor="0.999145" />
    </track>
    <track total="50" number="25" file="MRDTC - Brain talk (Bunker hellectric).flac">
      <integrated lufs="-7.79" lu="-15.21" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="26" file="Mechanic Doll Machine - Mirage.flac">
      <integrated lufs="-8.33" lu="-14.67" />
      <sample-peak spfs="0.00" factor="1.000031" />
    </track>
    <track total="50" number="27" file="Mentallo and The Fixer - Gammera (Equinox).flac">
      <integrated lufs="-6.47" lu="-16.53" />
      <sample-peak spfs="-0.01" factor="0.999023" />
    </track>
    <track total="50" number="28" file="Monospore - Standing high.flac">
      <integrated lufs="-8.62" lu="-14.38" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="29" file="Neonsol - Manipulation.flac">
      <integrated lufs="-7.12" lu="-15.88" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
    <track total="50" number="30" file="Neustrohm - Non toxic.flac">
      <integrated lufs="-8.88" lu="-14.12" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="31" file="Protectorate - Universal exports.flac">
      <integrated lufs="-7.59" lu="-15.41" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="32" file="Psychicold - Burn in Hell.flac">
      <integrated lufs="-8.39" lu="-14.61" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="33" file="Renoized - Defcon.flac">
      <integrated lufs="-8.42" lu="-14.58" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="34" file="Riotlegion - God(b)less.flac">
      <integrated lufs="-8.79" lu="-14.21" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="35" file="Sequenz_ - Despair (A.D.N. mix).flac">
      <integrated lufs="-9.00" lu="-14.00" />
      <sample-peak spfs="0.00" factor="1.000000" />
    </track>
    <track total="50" number="36" file="Shadow Lady - Species (Endzeit mix).flac">
      <integrated lufs="-6.52" lu="-16.48" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
    <track total="50" number="37" file="Shadow System - Nex.flac">
      <integrated lufs="-9.22" lu="-13.78" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="38" file="Statik Sky - Fuqin noize.flac">
      <integrated lufs="-8.39" lu="-14.61" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="39" file="Studio-X - The source of energy.flac">
      <integrated lufs="-6.26" lu="-16.74" />
      <sample-peak spfs="0.00" factor="1.000000" />
    </track>
    <track total="50" number="40" file="Suicidal Romance - Touch (ES23 mix).flac">
      <integrated lufs="-8.69" lu="-14.31" />
      <sample-peak spfs="-0.09" factor="0.990020" />
    </track>
    <track total="50" number="41" file="Switchface - A crack in the monolith.flac">
      <integrated lufs="-8.08" lu="-14.92" />
      <sample-peak spfs="0.00" factor="1.000000" />
    </track>
    <track total="50" number="42" file="Synapsyche - Breath control.flac">
      <integrated lufs="-9.48" lu="-13.52" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="43" file="Tamtrum - Le son de la pluie (Fils de pute mix by Grendel).flac">
      <integrated lufs="-7.78" lu="-15.22" />
      <sample-peak spfs="-0.50" factor="0.944060" />
    </track>
    <track total="50" number="44" file="Third Realm - Deliverance.flac">
      <integrated lufs="-7.40" lu="-15.60" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="45" file="Traumatize - Moje odbicie.flac">
      <integrated lufs="-9.20" lu="-13.80" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="46" file="Unter Null - The fall (The Chemical Sweet Kid mix).flac">
      <integrated lufs="-11.11" lu="-11.89" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="47" file="Vault 113 - The prophecy.flac">
      <integrated lufs="-9.16" lu="-13.84" />
      <sample-peak spfs="-0.22" factor="0.975188" />
    </track>
    <track total="50" number="48" file="[Fabrikmutter] - The minimal devotion.flac">
      <integrated lufs="-8.19" lu="-14.81" />
      <sample-peak spfs="0.00" factor="1.000031" />
    </track>
    <track total="50" number="49" file="[Product] - To the wind (chant).flac">
      <integrated lufs="-9.00" lu="-14.00" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
    <track total="50" number="50" file="[Synaptic_Reactor] - The devil's work (Ruinizer mix).flac">
      <integrated lufs="-10.23" lu="-12.77" />
      <sample-peak spfs="-0.22" factor="0.975176" />
    </track>
    <summary total="50">
      <integrated lufs="-8.00" lu="-15.00" />
      <sample-peak spfs="0.00" factor="1.000031" />
    </summary>
  </album>
</bs1770gain>

Traceback (most recent call last):
  File "/usr/local/bin/beet", line 11, in <module>
    sys.exit(main())
  File "/usr/local/lib/python2.7/dist-packages/beets/ui/__init__.py", line 1256, in main
    _raw_main(args)
  File "/usr/local/lib/python2.7/dist-packages/beets/ui/__init__.py", line 1243, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/usr/local/lib/python2.7/dist-packages/beets/ui/commands.py", line 955, in import_func
    import_files(lib, paths, query)
  File "/usr/local/lib/python2.7/dist-packages/beets/ui/commands.py", line 925, in import_files
    session.run()
  File "/usr/local/lib/python2.7/dist-packages/beets/importer.py", line 329, in run
    pl.run_parallel(QUEUE_SIZE)
  File "/usr/local/lib/python2.7/dist-packages/beets/util/pipeline.py", line 445, in run_parallel
    six.reraise(exc_info[0], exc_info[1], exc_info[2])
  File "/usr/local/lib/python2.7/dist-packages/beets/util/pipeline.py", line 312, in run
    out = self.coro.send(msg)
  File "/usr/local/lib/python2.7/dist-packages/beets/util/pipeline.py", line 194, in coro
    func(*(args + (task,)))
  File "/usr/local/lib/python2.7/dist-packages/beets/importer.py", line 1511, in plugin_stage
    func(session, task)
  File "/usr/local/lib/python2.7/dist-packages/beets/plugins.py", line 140, in wrapper
    return func(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/replaygain.py", line 1028, in imported
    self.handle_album(task.album, False)
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/replaygain.py", line 955, in handle_album
    album_gain = backend_instance.compute_album_gain(album)
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/replaygain.py", line 136, in compute_album_gain
    output = self.compute_gain(supported_items, True)
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/replaygain.py", line 189, in compute_gain
    return self.compute_chunk_gain(items, is_album)
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/replaygain.py", line 213, in compute_chunk_gain
    results = self.parse_tool_output(output, path_list, is_album)
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/replaygain.py", line 254, in parse_tool_output
    parser.Parse(text, True)
xml.parsers.expat.ExpatError: not well-formed (invalid token): line 11, column 44
```

### Setup

* OS: Linux Mint 18.3
* Python version: 2.7.12
* beets version: 1.4.8
* Turning off plugins made problem go away (yes/no): N/A

My configuration (output of `beet config`) is:

```yaml
beet config

paths:
    default: $artist/$album%aunique{}/$artist - $title
    singleton: $artist/$album%aunique{}/$artist - $title
    comp: $artist/$album%aunique{}/$artist - $title
threaded: yes
scrub:
    auto: no
chroma:
    auto: yes
library: ~/.config/beets/musiclibrary.db
replaygain:
    backend: bs1770gain
    method: ebu
    auto: yes
    overwrite: yes
    targetlevel: 89
    r128: [Opus]
    chunk_at: 5000

ui:
    color: yes

plugins: replaygain chroma duplicates fetchart web info
directory: ~/Musique

import:
    copy: no
    autotag: no
    resume: ask
    group_albums: yes
    bell: yes
duplicates:
    count: no
    full: no
    format: ''
    keys: []
    move: ''
    tag: ''
    path: no
    copy: ''
    tiebreak: {}
    album: no
    strict: no
    checksum: ''
    merge: no
    delete: no
web:
    cors_supports_credentials: no
    reverse_proxy: no
    host: 127.0.0.1
    include_paths: no
    cors: ''
    port: 8337
fetchart:
    auto: yes
    minwidth: 0
    sources:
    - filesystem
    - coverart
    - itunes
    - amazon
    - albumart
    google_engine: 001442825323518660753:hrh5ch1gjzm
    enforce_ratio: no
    cautious: no
    maxwidth: 0
    store_source: no
    google_key: REDACTED
    fanarttv_key: REDACTED
    cover_names:
    - cover
    - front
    - art
    - album
    - folder
```



Discussion:
It looks like `bs1770gain` is outputting invalid XML (even GitHub marks it as invalid!);

```XML
    <track total="50" number="3" file="Aim & Execute - Phantom energy.flac">
      <integrated lufs="-7.90" lu="-15.10" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
```

I'm pretty sure this should like this (and [xmlvalidation.com](https://xmlvalidation.com) agrees);

```XML
    <track total="50" number="3" file="Aim &amp; Execute - Phantom energy.flac">
      <integrated lufs="-7.90" lu="-15.10" />
      <sample-peak spfs="-0.03" factor="0.996765" />
    </track>
```
Aha! You know, sometimes I really miss XML. JSON doesn't have nearly as many fun surprises—`json.dumps` just works too reliably. :smiley:

The right way to proceed here would be to add a `try` block to catch the `ExpatError` and report an error.
@breversa can you check what version of `bs1770gain` you're on? [It seems they fixed a bug with XML escaping in v0.4.10](http://bs1770gain.sourceforge.net/#hist).

Running this should output the version you're on;

```Shell
dpkg -s bs1770gain | grep "^Version:"
```
@jackwilsdon, there you go :
`BS1770GAIN 0.4.8, Copyright (C) Peter Belkner 2014-2015.`

I'll upgrade my version and test again.
Darn… There's no upgraded version of bs177gain for my distro (since Linux Mint 19 has dropped KDE which I'm using). I guess I'll have to find another solution.

Can somebody else test meanwhile ?
Looks like you can download a pre-compiled version [here](https://sourceforge.net/projects/bs1770gain/files/bs1770gain/0.4.12/). Try downloading it somewhere and adding it to your `PATH`.

Resolving patch:
diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py
--- a/beetsplug/replaygain.py
+++ b/beetsplug/replaygain.py
@@ -251,7 +251,14 @@ def end_element_handler(name):
                 state['gain'] = state['peak'] = None
         parser.StartElementHandler = start_element_handler
         parser.EndElementHandler = end_element_handler
-        parser.Parse(text, True)
+
+        try:
+            parser.Parse(text, True)
+        except xml.parsers.expat.ExpatError:
+            raise ReplayGainError(
+                u'The bs1770gain tool produced malformed XML. '
+                'Using version >=0.4.10 may solve this problem.'
+            )
 
         if len(per_file_gain) != len(path_list):
             raise ReplayGainError(


Test patch:
diff --git a/test/test_replaygain.py b/test/test_replaygain.py
--- a/test/test_replaygain.py
+++ b/test/test_replaygain.py
@@ -19,7 +19,8 @@
 import unittest
 import six
 
-from test.helper import TestHelper, has_program
+from mock import patch
+from test.helper import TestHelper, capture_log, has_program
 
 from beets import config
 from beets.mediafile import MediaFile
@@ -44,6 +45,15 @@
     LOUDNESS_PROG_AVAILABLE = False
 
 
+def reset_replaygain(item):
+    item['rg_track_peak'] = None
+    item['rg_track_gain'] = None
+    item['rg_album_gain'] = None
+    item['rg_album_gain'] = None
+    item.write()
+    item.store()
+
+
 class ReplayGainCliTestBase(TestHelper):
 
     def setUp(self):
@@ -68,20 +78,12 @@ def setUp(self):
 
         album = self.add_album_fixture(2)
         for item in album.items():
-            self._reset_replaygain(item)
+            reset_replaygain(item)
 
     def tearDown(self):
         self.teardown_beets()
         self.unload_plugins()
 
-    def _reset_replaygain(self, item):
-        item['rg_track_peak'] = None
-        item['rg_track_gain'] = None
-        item['rg_album_gain'] = None
-        item['rg_album_gain'] = None
-        item.write()
-        item.store()
-
     def test_cli_saves_track_gain(self):
         for item in self.lib.items():
             self.assertIsNone(item.rg_track_peak)
@@ -166,6 +168,46 @@ class ReplayGainLdnsCliTest(ReplayGainCliTestBase, unittest.TestCase):
     backend = u'bs1770gain'
 
 
+class ReplayGainLdnsCliMalformedTest(TestHelper, unittest.TestCase):
+
+    @patch('beetsplug.replaygain.call')
+    def setUp(self, call_patch):
+        self.setup_beets()
+        self.config['replaygain']['backend'] = 'bs1770gain'
+
+        # Patch call to return nothing, bypassing the bs1770gain installation
+        # check.
+        call_patch.return_value = None
+        self.load_plugins('replaygain')
+
+        for item in self.add_album_fixture(2).items():
+            reset_replaygain(item)
+
+    @patch('beetsplug.replaygain.call')
+    def test_malformed_output(self, call_patch):
+        # Return malformed XML (the ampersand should be &amp;)
+        call_patch.return_value = """
+            <album>
+                <track total="1" number="1" file="&">
+                    <integrated lufs="0" lu="0" />
+                    <sample-peak spfs="0" factor="0" />
+                </track>
+            </album>
+        """
+
+        with capture_log('beets.replaygain') as logs:
+            self.run_command('replaygain')
+
+        # Count how many lines match the expected error.
+        matching = [line for line in logs if
+                    line == 'replaygain: ReplayGain error: bs1770gain '
+                            'returned malformed XML - this is a bug in '
+                            'versions prior to v0.4.10, please ensure that '
+                            'your version is up to date']
+
+        self.assertEqual(len(matching), 2)
+
+
 def suite():
     return unittest.TestLoader().loadTestsFromName(__name__)