Bitcrush.io

After much lamenting and gnashing of teeth, I've successfully created a macOS LaunchAgent (tested on 12.6.7) which periodically checks the current forwarded VPN port set by the WireGuard application and updates it in the qBittorrent configuration.

I thought I had a good lead when I discovered you could launch qBittorrent from the Terminal with the --listen_port option to set a new incoming connection port, but that doesn't help when qBittorrent is already running. I found somebody suggesting having a script periodically shut down qBittorrent completely, change the port, and then relaunch, but I thought that was too much of a kludge.

Thankfully I discovered the built-in Web UI. You can enable this in the qBittorrent preferences, and it provides an API accepting GET and POST requests for all of the configuration options. You can visit the Web UI in a browser at http://localhost:8080 if you use the default settings, and we'll be using the API endpoint http://localhost:8080/api/v2/app/setPreferences.

Here's what I ended up with:

#!/bin/bash
NATPMP=$(/usr/bin/python3 /Users/bitcrush/Library/Python/3.9/bin/natpmp-client.py -l 999 -g 10.2.0.1 0 0)
PORTF=$(echo $NATPMP | sed 's/.*private_port\ \(.....\),.*/\1/')
curl -i -X POST -d "json=%7B%22listen_port%22%3A${PORTF}%7D" http://localhost:8080/api/v2/app/setPreferences

The first line is a call to natpmp-client, which is part of the py-natpmp Python package, storing its default output in the variable $NATPMP. These particular settings are being used to find the current port mapping set on my WireGuard Tunnel, which is managed separately in the WireGuard application.

The second line pipes $NATPMP to sed, which uses a regular expression to find the 5 characters after "private_port " in the first line's output, and then stores those characters in the variable $PORTF.

Finally, the third line uses curl to send an HTTP POST request to http://localhost:8080/api/v2/app/setPreferences with the body

json=%7B%22listen_port%22%3A${PORTF}%7D

This looks a bit strange because it's using URL Encoding for some characters, which prevents the browser or the server from intepreting them as having special meaning in HTML. %7B stands in for {, %22 stands in for a double-quote ", and so on. Once it's decoded, you'll see it's just standard JSON in one line:

json={"listen_port":${PORTF}}

and once the bash shell performs its parameter expansion, ${PORTF} will become e.g. "47872" and thus the body of the POST request finally becomes

json={"listen_port":47872}

Open up preferences in your qBittorrent GUI, and you should see that the incoming connection port has been changed. You can test this by commenting out the second line and adding a line immediately after:

PORTF=55555

I've put this script as 'portf.sh' in my home folder, and made sure to set it as executable using

bitcrush@MacPro ~ % chmod +x portf.sh

Now how to get it automated? On macOS, you could use cron like most people would on Unix/Linux, but Apple also added their own method called "LaunchAgents" and "LaunchDaemons". In our case, we just want a simple LaunchAgent for my user only, so I'm going to make a new .plist file:

bitcrush@MacPro ~ % nano /Users/bitcrush/Library/LaunchAgents/com.bitcrush.portf.plist

You can copy and paste this code and then modify for your own details:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Label</key>
        <string>com.bitcrush.portf</string>
        <key>ProgramArguments</key>
        <array>
                <string>/bin/bash</string>
                <string>/Users/bitcrush/portf.sh</string>
        </array>
        <key>StartInterval</key>
        <integer>10</integer>
</dict>
</plist>

After modifying com.owner.yourscriptname, and /Users/yourusername/yourscriptname.sh, press Ctrl+O to save the file and then Ctrl+X to exit the nano text editor. Obviously you can make those things anything you like to match your setup, and the StartInterval can be set to X number of seconds. I've used 10 here but you could probably set it much higher such as 600 for 10min, given that the port from WireGuard plus my VPN (ProtonVPN) doesn't change that frequently.

To make sure it's working, we can run the following commands:

bitcrush@MacPro ~ % launchctl load ~/Library/LaunchAgents/com.bitcrush.portf.plist
bitcrush@MacPro ~ % launchctl start ~/Library/LaunchAgents/com.bitcrush.portf.plist
bitcrush@MacPro ~ % launchctl list | grep bitcrush

This last command will pipe the current list of running LaunchAgents for your user into grep, filtering out everything but the line with your name in it, and then printing that to STDOUT:

-   0   com.bitcrush.portf

If you see a number other than 0, something has gone wrong and this number is the exit code. In my case, I first saw '127', which meant that something in my portf.sh script wasn't working correctly. I went back in to comment out the lines one by one and fix the issue (a typo), and then ran:

bitcrush@MacPro ~ % launchctl unload ~/Library/LaunchAgents/com.bitcrush.portf.plist
bitcrush@MacPro ~ % launchctl load ~/Library/LaunchAgents/com.bitcrush.portf.plist
bitcrush@MacPro ~ % launchctl start ~/Library/LaunchAgents/com.bitcrush.portf.plist
bitcrush@MacPro ~ % launchctl list | grep bitcrush

Once you've got a 0 exit code, you should be able to look in your qBittorrent preferences and see that the port number has changed. Congrats! Now you never have to think about this again, right?