Grooving with Camp Counselor: Your Bandcamp Buddy

Ah, the sweet siren call of new music! If there’s one thing I’m always on the prowl for, it’s fresh tunes to tickle my eardrums. I’m practically the Bandcamp connoisseur, the aficionado of audio, if you will. My collection’s like a treasure trove of musical gems, all handpicked from the vast Bandcamp galaxy. But there’s a twist to this tale – my wishlist isn’t just a shopping list; it’s a roadmap to musical exploration.

Picture this: thousands of albums languishing in my wishlist, each one a potential masterpiece. It’s a labyrinth of sound, and in my quest for auditory bliss, I’ve encountered a conundrum. How do I keep track of what I’ve listened to and what piqued my interest? You see, I’m not just a ‘skip and forget’ kind of listener. Even if an album doesn’t quite hit the mark, it lingers on my wishlist, a musical enigma begging to be solved. But here’s the kicker – without notes, it’s like trying to recall the lyrics of a song from a dream. Frustrating, right?

So, in the spirit of ‘necessity is the mother of invention,’ I decided to craft a digital companion to my Bandcamp odyssey. Behold, Camp Counselor! This nifty tool syncs your Bandcamp wishlist and purchase history into a cozy database, giving you the power to annotate, star, and sort your albums with reckless abandon.

Now, I know what you’re thinking: “Why not just leave a public review on Bandcamp?” Well, my friend, sometimes the musical journey is a private affair, a secret rendezvous between you and your headphones. That’s where Camp Counselor shines. It’s your personal backstage pass to the music world. You can leave your thoughts, ratings, and quirks in blissful secrecy.

Now, here’s the cherry on top – I wanted to break the mold, to venture into new territories. This wasn’t just about crafting a web app; it was about exploring uncharted sonic realms. So, Camp Counselor is tailored for my trusty Pinephone, my loyal companion on my musical journeys. I built it with Vala and Gtk4 for Linux, complete with a slick and responsive design. As for Mac and Windows users, well, I haven’t given it a whirl, and truth be told, I’m dancing to a different beat.

But wait, there’s more! In the future, I might just sprinkle some more magic into Camp Counselor:

  • Imagine the sheer delight of previewing albums right within the app, eliminating the need to hop between tabs.
  • Ever wish you could magically add new albums from artists you adore to your wishlist? Well, I’m conjuring up just that.
  • Who knows? Camp Counselor might just evolve to embrace the vast world of musical data beyond Bandcamp.

So there you have it, folks – Camp Counselor, your trusty sidekick in the realm of Bandcamp. It’s time to dive into the sonic sea, navigate the waves of melodies, and rediscover your musical journey, one note at a time. Happy listening! 🎵🎉

Converting Bandcamp Email Updates to an RSS Feed

I love music. I like to closely track bands and labels to learn about upcoming releases. Unfortunately, the type of music I listen to tends to stay more underground, so there isn’t any mainstream coverage of it, which just means I get to do all the work myself.

Fortunately, a majority of the bands and labels I am interested in are on bandcamp. Bandcamp makes it pretty easy to subscribe to artists and labels and get updates from them directly. Unfortunately, this all comes through email. While it is easy enough to filter to avoid cluttering my inbox, email isn’t my desired place for this information.

I run a private freshrss instance and curate several hundred RSS feeds. It syncs nicely between my various devices, and is just a great way to consume. My goal is to get all my music updates from bandcamp here, so that’d they be in the same place as where I get music reviews and follow some other underground music blogs.

Bandcamp doesn’t provide any rss feeds. There are some options, like using RSSHub and its bandcamp addon. However, many of the updates that come through email are private, and don’t show up in an artists’ or labels’ feed.

I decided, I’d go a different route, and just convert the emails I was getting into an RSS feed directly.

Step 1 – imapfilter

I already use imapfilter heavily for filtering and tagging my emails. I decided I could use it to pipe the bandcamp emails to a custom script that would convert it into an rss entry. Here is the relevant section:

function filter_bandcamp(messages)
   -- Grab anything from Bandcamp where the subject
   --  starts with "New". This indicates a message
   --  or updated rather than a receipt.
   -- I am taking these "mailing list" bandcamps
   --  messages and converting them to an rss feed.
   results = messages:contain_from('') *

   for _, mesg in ipairs(results) do
      mbox, uid = table.unpack(mesg)
      text = mbox[uid]:fetch_message()
      pipe_to('/opt/email2rss/email2rss', text)

   -- delete the bandcamp messages

   return messages;

You’ll notice I don’t want all messages from “”, since that would also include things like purchases and system notifications.

Step 2 – email2rss script

The email2rss script is a python program. I am using feedgen to generate the feed, and built in python libraries to parse the emails.

One issue you’ll notice immediately, is that this script runs once for every email message. For RSS, we want to create a continuous feed with all the entries. This means we have to insert the new entry into an existing file and have some persistence. The quickest/dirtiest method was to use python’s builtin pickle to serialize and deserialize the whole state. That way, I can quickly load the previous state, create a new entry, write out the rss file, then serialize and save the state to disk.

Here is the program in its entirety:

#!/usr/bin/env python3

import sys
import email.parser
import datetime
import pickle
import re
import os
from feedgen.feed import FeedGenerator

DATADIR=os.path.join('/', 'opt', 'email2rss', 'data')

def default_feed():
  fg = FeedGenerator()'')
  fg.title('Bandcamp Updates')
  fg.description('Bandcamp Updates') = '')

  return fg

def get_feedgenerator():
    with open(os.path.join(DATADIR, 'feed.obj'), 'rb') as f:
      return pickle.load(f)
  except IOError:
    return default_feed()

def save_feedgenerator(fg):
  with open(os.path.join(DATADIR, 'feed.obj'), 'wb') as f:
    pickle.dump(fg, f)

def add_item(fg, msg, content):
  msg_id_header = msg.get('Message-ID')
  msg_id = re.match(r'^\<(.*?)@.*$', msg_id_header).group(1)
  sender = msg.get('From')
  subject = msg.get('Subject')

  fe = fg.add_entry()'{msg_id}')
  fe.title(subject) = 'Bandcamp', email = '')
  fe.content(content, type = 'CDATA')

def go():
  fg = get_feedgenerator()

  parser = email.parser.Parser()
  msg = parser.parse(sys.stdin)
  for part in msg.walk():
    if part.get_content_type() == 'text/html':
      add_item(fg, msg, part.get_payload())

  fg.rss_file(os.path.join(DATADIR, 'feed.rss'), pretty = True)


if __name__ == '__main__':

Step 3 – Host the Feed

I just have a simple nginx server running that hosts the feed.rss. Then I simply add this new feed to my freshrss instance.

Future Work

There are still some improvements that could be made to this. The history is going to grow out of control at some point, so I really should probably go through and delete old entries, maybe to keep a history of 100 or 500 entries.

The other possible issue (I haven’t run into it yet), is that the email2rss could be run simultaneously. If that is the case, then one entry will likely be lost. I really should probably have a lock around the feed.obj to keep a second instance from doing anything until the first was written out the new state.

Fixing the Audio on a Pinebook Pro after resume

I recently purchased a Pinebook Pro, which is a Linux laptop based on the Arm processor. It is a great travel laptop, and surprisingly, I have found this to now be my primary personal device.

Everything has worked well, except one annoying bug: after resuming from sleep, the audio stops working. After digging around, I found that a simple command to write some data to the device to unbind it, then rebind it resolves it. I incorporated this into a systemd script that it is automatically run upon resume.

Create a new file called: /usr/lib/systemd/system-sleep/audio with the following (sudo vim /usr/lib/systemd/system-sleep/audio)

# mwd - 20200726
# This is a script to reload the audio module
#  upon resume.
case $1 in
        tee /sys/bus/i2c/drivers/es8316/{un,}bind <<< 1-0011

Save this file and make sure it is executable:

sudo chmod +x /usr/lib/systemd/system-sleep/audio

Supporting Deep links in Linux with Gnome and Epiphany Web Apps

With the recent arrival of gnome 3.28, and with it, the Epiphany Web browser 3.28, saving a web page as a webapp has dramatically improved.  Saving a page as a webapp has several advantages:

  • Cookies are separate for each app. This helps limit tracking, but can also make managing multiple accounts much easier, like separating out your person gmail account with a work gmail account.
  • A clear separation of tasks. I spend a lot of time in Jira, confluence, and the AWS console. Keeping them as separate applications instead of digging through 50 tabs in firefox, is very useful.
  • They are treated as completely separate apps. This means that they have their own launcher with their own applications, can be pinned to the dock, and show up as separate applications in the window switcher

Each web app shows up separately in the alt-tab window switcher

Each Web App shows up as a separated application that can be pinned in the dock on the left.

Creating a web app in Epiphany is as simple as browsing to the website you want, clicking on the Menu button (三), and choose ‘Install Site as Web Application…’

Deep Links

iOS and Android support the concept of Deep Linking, also sometimes known as Universal Links. The idea, is that if you click on a link for a specific website, say Reddit, the operating system can decide to open that link with the installed Reddit App instead of the web browser.

I have found this to be a nice concept. When I get an email or slack message with a link to a Jira ticket, I want it to open up with my Jira web app, not my default web browser. After all, that was part of the point of having created these separate Web Apps. Unfortunately, there is no native support for this feature, but, since Linux is an open and configurable system, there is always a way!

Creating a new Default Web Browser

Gnome only allows registering applications based on the MIMEType. Unfortunately, all websites share the same scheme: ‘x-scheme-handler/http’. By default, gnome opens up your default browser and passes it the URL. We will create a new simple shell script and make it our default web browser. This simple shell script can do some basic parsing of URLs, then decide to open up a specific web app or to pass everything on to firefox.

A .desktop file is required to register our new App. Create a new file called:


We will add the following to it:

[Desktop Entry]
Name=HTTP Handler
Exec=/home/username/.local/bin/http-handler %u
This declares that it can handle the necessary MimeTypes for URLs. You will also notice that it executes /home/username/.local/bin/http-handler. This is the script we are going to create next, however, please change the username to be your username.

Building the Web App Script

I am just using a simple bash script to parse the URL and decide whether to open a specific web app or to open the default web browser. You can modify this or write it in any language that you want. This is just mean to be a simple example.

Create a new file named `~/.local/bin/http-handler` with the following:

#!/usr/bin/env bash

## Regular Expression Matches ##

# Jira
JIRA_RE=$(echo "$URL" | gawk '/^https?:\/\/company\.atlassian\.net\/.*/&&!/^https?:\/\/company\.atlassian\.net\/wiki.*/ {print "yes"}')
JIRA_EXEC="gtk-launch epiphany-jira-url"

AWS_RE=$(echo "$URL" | gawk '/^https?:\/\/console\.aws\.amazon\.com\/.*/ {print "yes"}');
AWS_EXEC="gtk-launch epiphany-aws-helplightning-url"

if [ x$JIRA_RE = "xyes" ]; then
 exec $JIRA_EXEC "$URL"
elif [ x$AWS_RE = "xyes" ]; then
 exec $AWS_EXEC "$URL"
 exec gtk-launch firefox $URL

Make sure you make your script executable:

chmod +x ~/.local/bin/http-handler

My script looks to see if a URL matches my Jira url, and if so, opens that webapp. Otherwise, it checks to see if it matches AWS console, and opens my AWS webapp. Otherwise, it runs firefox and opens the URL in a new window.

While there are many ways to parse the URL, this one uses gawk so we can test some regular expressions. The Jira URL was interesting because I want everything that starts with* except* since that is confluence, a separate app.

One thing about Epiphany Web Apps, is that there isn’t an easy way to run them directly. They must be executed through their .desktop file. This is so that they set the correct WM class so that the desktop environment knows to treat them as a separate application. Fortunately, we can use the gtk-launch program which takes a .desktop file (minus the .desktop) as a parameter, and launches it.

Fixing the .desktop files

When you install a Web App, epiphany creates a new desktop file in `~/.local/share/applications/` with a name like `epiphany-website-a8d40c5d8d04433783264b93849886b867fa11c4.desktop`.  These desktop files have the website hard coded into the Exec line. This means that we can’t pass it a URL, so it always opens up to the default page, which isn’t what we want. I haven’t yet found a way to have a default parameter that can be overridden. The trick is to create a copy of the .desktop file that does take a URL as a parameter. This means we will end up with two launchers, but we’ll set the NoDisplay parameter in our new one, so that it doesn’t show up in the launcher screen.

Copy the webapp .desktop file to a new name, something like:

cp epiphany-jira-b25783a8e196a10e6c869d284bdfff09546e43d6.desktop epiphany-jira-url.desktop

Edit your new `eiphany-jira-url.desktop` file. We are going to change the Exec line to take a URL parameter, and we are going to set the NoDisplay=true:

[Desktop Entry]
Exec=epiphany --application-mode --profile="/home/dillavou/.config/epiphany/app-epiphany-jira-b25783a8e196a10e6c869d284bdfff09546e43d6" %u

We can now test this new launcher by using the gtk-launch app and passing in the name of our new .desktop file:

gtk-launch eipiphonay-jira-url

Now make sure in your `http-handler` script, that you are executing your new .desktop file!

You can test your script by running:


to make sure it opens your web app. And, you can test the fallback to your web browser by running:


Set your Default Web Browser

With our script now working, the last step is to make our http-handler program the default web browser. This can be done on the command line by running:

xdg-settings set default-web-browser http-handler.desktop

If that doesn’t work, open up the gnome settings, go to Details -> Default Applications and make sure Web is set to ‘HTTP Handler’.

You can now test from the command line by running:


or by clicking on a link in an email, text, instant message, etc…

L2TP VPN Server on Raspberry Pi

The following are instructions for setting up an L2TP VPN server on a Raspberry Pi running Raspbian Jessie. This allows you to connect your iPhone or other device using L2TP VPN to your home network, to securely access resources on it. This set up uses a Raspberry Pi sitting behind your normal router.

These instructions are based on an older forum post on the Raspberry Pi forums.

All of the following commands will need to be run as root. Use sudo to become the root user.

$ sudo su -

Configuring a Static IP Address

Since your Raspberry Pi is running a server, it will be important to give it a consistent IP address so that we can forward the necessary ports to it. The IP address you choose depends on your local network setup. My network uses the 192.168.1.XXX range, so I have decided to use for my Raspberry Pi. Here are the full settings for my setup:

IP Address:



DNS Server(s):

With the release of Raspbian Jessie, the method for configuring IP addresses has changed. Raspbian now uses dhcpcd as the default, so it is no longer recommended that you directly modify /etc/network/interfaces. Instead, we will modify dhcpcd’s configuration.

Edit /etc/dhcpcd.conf and add the following to the end. You will need to modify some of these values based on your setup.

interface eth0
static ip_address=
static routers=
static domain_name_servers=

Once you reboot, your Raspberry Pi should now be using the address you have specified.

Installing xl2tpd and openswan

We need to install xl2tpd for our VPN tunnel and openswan for our IPSec security.

Warning: openswan is no longer maintained and has been replaced by strongswan. I have not yet tried this with strongswan.

$ apt-get update
$ apt-get install openswan xl2tpd ppp lsof

Configuring xl2tpd

xl2tpd provides our VPN tunnel into our network.

Replace the contents of /etc/xl2tpd/xl2tpd.conf with the following. You may need to make changes based on your network settings and your static IP address we configured previously.

ipsec saref = yes
listen-addr =

[lns default]
ip range =
local ip =
assign ip = yes
require chap = yes
refuse pap = yes
require authentication = yes
name = linkVPN
ppp debug = yes
pppoptfile = /etc/ppp/options.xl2tpd
length bit = yes

Replace your /etc/ppp/options.xl2tpd with the following:

asyncmap 0
idle 1800
mtu 1200
mru 1200
name l2tpd
lcp-echo-interval 30
lcp-echo-failure 4
connect-delay 5000

Configuring IPSec

IPSec is the encryption layer for your VPN tunnel. We are using the openswan implementation.

Replace your /etc/ipsec.conf with the following. Again, you will need to replace any values depending on your network setup.

# /etc/ipsec.conf - Openswan IPsec configuration file

# This file:  /usr/share/doc/openswan/ipsec.conf-sample
# Manual:     ipsec.conf.5

version    2.0    # conforms to second version of ipsec.conf specification

# basic configuration
config setup
    # Do not set debug options to debug configuration issues!
    # plutodebug / klipsdebug = "all", "none" or a combation from below:
    # "raw crypt parsing emitting control klips pfkey natt x509 dpd private"
    # eg:
    # plutodebug="control parsing"
    # Again: only enable plutodebug or klipsdebug when asked by a developer
    # enable to get logs per-peer
    # plutoopts="--perpeerlog"
    # Enable core dumps (might require system changes, like ulimit -C)
    # This is required for abrtd to work properly
    # Note: incorrect SElinux policies might prevent pluto writing the core
    # NAT-TRAVERSAL support, see README.NAT-Traversal
    # exclude networks used on server side by adding %v4:!a.b.c.0/24
    # It seems that T-Mobile in the US and Rogers/Fido in Canada are
    # using 25/8 as "private" address space on their 3G network.
    # This range has not been announced via BGP (at least upto 2010-12-21)
    # OE is now off by default. Uncomment and change to on, to enable.
    # which IPsec stack to use. auto will try netkey, then klips then mast
    # Use this to log to a file, or disable logging on embedded systems (like openwrt)

# Add connections here

# sample VPN connection
# for more examples, see /etc/ipsec.d/examples/
#conn sample
#        # Left security gateway, subnet behind it, nexthop toward right.
#        left=
#        leftsubnet=
#        leftnexthop=
#        # Right security gateway, subnet behind it, nexthop toward left.
#        right=
#        rightsubnet=
#        rightnexthop=
#        # To authorize this connection, but not actually start it, 
#        # at startup, uncomment this.
#        #auto=add

    # !mwd - disabling this fixed stuff

conn L2TP-PSK-noNAT
        # we cannot rekey for %any, let client rekey
        # Apple iOS doesn't send delete notify so we need dead peer detection
        # to detect vanishing clients
        # Set ikelifetime and keylife to same defaults windows has
        # l2tp-over-ipsec is transport mode
        # For updated Windows 2000/XP clients,
        # to support old clients as well, use leftprotoport=17/%any
        # The remote user.
        # Using the magic port of "%any" means "any one single port". This is
        # a work around required for Apple OSX clients that use a randomly
        # high port.
        #force all to be nat'ed. because of ios

# Normally, KLIPS drops all plaintext traffic from IP's it has a crypted
# connection with. With L2TP clients behind NAT, that's not really what
# you want. The connection below allows both l2tp/ipsec and plaintext
# connections from behind the same NAT router.
# The l2tpd use a leftprotoport, so they are more specific and will be used
# first. Then, packets for the host on different ports and protocols (eg ssh)
# will match this passthrough conn.
conn passthrough-for-non-l2tp

Configuring your Secret Key

The secret key is a shared key that all of your users will use. Edit /etc/ipsec.secrets

# This file holds shared secrets or RSA private keys for inter-Pluto
# authentication.  See ipsec_pluto(8) manpage, and HTML documentation.

# RSA private key for this host, authenticating it to any other host
# which knows the public part.  Suitable public keys, for ipsec.conf, DNS,
# or configuration of other implementations, can be extracted conveniently
# with "ipsec showhostkey".

# this file is managed with debconf and will contain the automatically created RSA keys
#include /var/lib/openswan/  %any:   PSK "MYSECRET"

Configuring your Users

You can create as many vpn users as you want. These users are separate from any linux user accounts on your Raspberry Pi. Edit /etc/ppp/chap-secrets

# Secrets for authentication using CHAP
# client    server    secret            IP addresses
username    *    password    *

Modifying iptables and System Services

We need to make some changes to the routing table and system configuration. First we will set some values and add them to our /etc/sysctl.conf which will be loaded each time the system starts up:

$ echo "net.ipv4.ip_forward = 1" |  tee -a /etc/sysctl.conf
$ echo "net.ipv4.conf.all.accept_redirects = 0" |  tee -a /etc/sysctl.conf
$ echo "net.ipv4.conf.all.send_redirects = 0" |  tee -a /etc/sysctl.conf

$ sysctl -p

The iptables and /proc settings won’t survive a reboot. We’ll add these commands to the end of our /etc/rc.local to make sure they are executed on start up:

for vpn in /proc/sys/net/ipv4/conf/*; do echo 0 > $vpn/accept_redirects; echo 0 > $vpn/send_redirects; done
iptables --table nat --append POSTROUTING --jump MASQUERADE

Finally, let’s make sure our xl2tpd and ipsec services will be started on boot:

$ update-rc.d -f ipsec remove
$ update-rc.d ipsec defaults

At this point, you should restart your Raspberry Pi to make sure all settings have taken effect and is configured correctly.

Configuring your Router Port Forwarding

This section depends on your router. Most consumer routers/wifi have a web admin interface at either or Once you are logged in, you’ll need to find the port forwarding or NAT/Gaming section. You will need to have the following ports forwarded to your Raspberry Pi IP Address, which in my case is Please be aware that these ports are UDP not TCP.

Port 4500 UDP

Port 500 UDP

Connecting an iPhone

On your iPhone, go to Setting > General > VPN

Choose “Add VPN Configuration”. Select “L2TP” as the Type.

Type: L2TP

Description: Home VPN

Server: Your public IP address (this is NOT your address. You can get this from your router or from

Account: The username you configured in /etc/ppp/chap-secrets

RSA SecureID: Disabled

Password: The password you configured in /etc/ppp/chap-secrets

Secret: The Shared secret you configured in /etc/ipsec.secrets

Send All Traffic: If enabled, then ALL your internet traffic will be routed through your home network. If you disable this, then normal internet traffic won’t go through your home network. The VPN will only be used to access devices on your home network