Auto muting when spotify plays an advertisement

Bharat Kalluri / 2020-12-31

I did not buy spotify premium for this year (yet?). I'm not sure if it is just me or it is happening for everybody, but it looks like for some reason I'm bombarded with ads. I get 2-3 ads between every 3-4 songs. They are boring, repetitive and even sometimes in other languages. I understand that the idea of ads is to push people to pay, so I guess they are successful in that regard.

Note (from me in the future) that I am a paid subscriber for spotify. But still there is value in working on hacks like these for fun & learning!

So, lets make linux mute audio for us when a spotify advert is playing!

Understanding d-bus and MPRIS

Some context before I start showing how I solved this. There is a message bus system in linux called d-bus. d-bus is a free desktop standard and is behind almost all the communication which happens between programs.

One more important piece is called MPRIS. MPRIS stands for Media Player Remote Interfacing Specification. It is a standard d-bus interface which aims to produce common programmatic API for controlling media players (This is straight up copied from the spec). This is also a freedesktop spec. This is the beauty of specs and the free desktop, all the famous desktops adhere to this standard and it is well documented.

Recognizing when spotify plays an advert

All we have to do is have a small function run every second, check what media is playing by name. If it turns out to be an "Advertisement", then we just mute the audio source. I am running ALSA (Advanced Linux Sound Architecture) in the background and it can be controlled by amixer.

mute_ad_spotify() {
    MEDIA_TITLE=$(dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get string:org.mpris.MediaPlayer2.Player string:Metadata 2>/dev/null | sed -n '/title/{n;p}' | cut -d '"' -f 2)

    if [[ $MEDIA_TITLE = "Advertisement" ]]; then
        amixer -q -D pulse sset Master mute
    else
        amixer -q -D pulse sset Master unmute
    fi
}

The important command to understand is in line two, Let's break it down.

dbus-send \
    --print-reply \
    --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get string:org.mpris.MediaPlayer2.Player string:Metadata
  • This sends a command to d-bus asking for mpris properties of spotify. This will return a bunch of values like track ID, length, album, albumArtist etc... Run this command when running spotify to get a taste of what data is available for us to play with. (ref)
  • Also forwarding the stderr to /dev/null, using 2>/dev/null. The "right" way to do it is to check if spotify is even running and if yes, ping the d-bus. But that is too many commands for something which runs every second. So whenever spotify is not running, the command will error out and the error will be sent to the void.
  • sed -n '/title/{n;p}' The -n flag suppress all output that isn't explicitly printed. n in the flower brackets moves to the next line and p prints it explicitly.(ref)
  • At this point we will be left with variant string "<media name>". cut is used with a delimiter(-d) of " and the first (zero ordered) piece is taken out using -f

Now, the media title is compared to string "Advertisement" (that's what spotify seems to send to MPRIS if it is playing an ad). If true, then we mute the sound using amixer -q -D pulse sset Master mute else unmute.

Finally we need to place it somewhere such that it runs on startup. Not in .bashrc/.zshrc, because they only run when the shell starts. To run it when the system starts, this trigger should be placed in .profile. Let us add a loop in .profile to run the function every second.

# .profile
# Mute spotify on ad
while sleep 1; do mute_ad_spotify; done &

That's it! Now whenever spotify plays an ad, the audio is muted automatically.

Why can I not mute audio at all!?

The previous solution works, but is a very bad take. Every second, it checks if the current playing music is named "Advertisement" and if it is then mutes it. But the problem is, now the audio can never be manually muted. Since every time audio is manually muted, the next second the script runs -> sees the media name is not Advertisement and un-mutes!

So one sanity check to add is to make sure spotify is actually running and then check if the music played by spotify is an ad and mute it. This is still a problem, I will explain why later. Let us look at the modified function now.

# Mutes audio if spotify ad is playing
mute_ad_spotify() {
    MEDIA_TITLE=$(dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get string:org.mpris.MediaPlayer2.Player string:Metadata 2>/dev/null | sed -n '/title/{n;p}' | cut -d '"' -f 2)

    IS_MUTE=0
    [[ $(amixer get Master | sed "5q;d") == *off* ]] && IS_MUTE=1

    IS_SPOTIFY_AD=0
    [[ $MEDIA_TITLE == *Advertisement* ]] && IS_SPOTIFY_AD=1

    if [ ! -z "$MEDIA_TITLE" ]; then
        if [[ $IS_MUTE == 0 ]] && [[ $IS_SPOTIFY_AD == 1 ]]; then
            amixer -q -D pulse sset Master mute
        elif [[ $IS_MUTE == 1 ]] && [[ $IS_SPOTIFY_AD == 0 ]]; then
            # unmutes if mixer is muted and no ad is playing, this ensures that after ad the music will continue
            # But also this means that when spotify is playing, I cannot mute audio!
            amixer -q -D pulse sset Master unmute
        fi
    fi

}

Two booleans, first (IS_MUTE) checks if the audio is muted and second (IS_SPOTIFY_AD) checks if the current playing spotify song name is called "Advertisement". First check is if spotify is even playing, later if not muted and spotify ad then mute else unmute. This ensures that when spotify is not playing, mute works as expected.

But there are problems to this as well. If spotify is paused and firefox is playing audio, then mute no longer works as expected. You also cannot mute a song when spotify is playing 😆. The ideal way would be to just deal with the spotify audio stream and manage it, I will refine this later on at some point.

The solution

This is probably the right way to do it. Instead of dealing with the entire sound card, we now deal only with spotify input sink. Get the right pulse audio input sink ID and mute that. Pulse Audio is the sound server distributed by the free desktop.

# Mutes audio if spotify ad is playing
mute_ad_spotify() {

    SPOTIFY_SINK_ID=$(pacmd list-sink-inputs | tr '\n' '\r' | perl -pe 's/ *index: ([0-9]+).+?application\.name = "([^\r]+)"\r.+?(?=index:|$)/\2:\1\r/g' | tr '\r' '\n' | grep spotify | cut -d ":" -f 2)

    if [ ! -z "$SPOTIFY_SINK_ID" ]; then
        MEDIA_TITLE=$(dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get string:org.mpris.MediaPlayer2.Player string:Metadata 2>/dev/null | sed -n '/title/{n;p}' | cut -d '"' -f 2)

        IS_SPOTIFY_AD=0
        [[ $MEDIA_TITLE == *Advertisement* ]] && IS_SPOTIFY_AD=1

        if [[ $IS_SPOTIFY_AD == 1 ]]; then
            pactl set-sink-input-mute ${SPOTIFY_SINK_ID} 1
        elif [[ $IS_SPOTIFY_AD == 0 ]]; then
            pactl set-sink-input-mute ${SPOTIFY_SINK_ID} 0
        fi
    fi

}

I'll be honest, I have no idea what the perl magic is in the command which retrieves the spotify sink ID. I just copied it from stackoverflow and did a grep | cut to get the sink number. Later on when an Advertisement is playing, I'm using the pactl to mute the input audio sink. This seems to work as expected.

Bash with the unix utilities is a killer combo, I should probably write an article explaining the basics of bash and some handy unix utilities.

Hand crafted by Bharat Kalluri