diff --git a/cmd/radioctl/radioctl.go b/cmd/radioctl/radioctl.go
index 5105d9cec47de998c42f72144139a2ef16cc25d9..a9e681321d6b860d0e715cd74e53c4bf851530c5 100644
--- a/cmd/radioctl/radioctl.go
+++ b/cmd/radioctl/radioctl.go
@@ -5,66 +5,39 @@ import (
 	"fmt"
 	"hash/crc32"
 	"log"
-	"os"
-	"path/filepath"
 	"net/url"
+	"os"
 	"strings"
 
 	"git.autistici.org/ale/autoradio"
+	"github.com/gonuts/commander"
+	gonutsflag "github.com/gonuts/flag"
 )
 
-// Describes the syntax of a command.
-type Command struct {
-	Name     string
-	UsageStr string
-	Help     string
-	Fn       func([]string)
-	MinArgs  int
-	MaxArgs  int
+type HasAddFlags interface {
+	AddFlags(*gonutsflag.FlagSet)
 }
 
-func (c Command) PrintHelp() {
-	fmt.Fprintf(os.Stderr, "%s %s\n  %s\n\n", c.Name, c.UsageStr, c.Help)
+type CommandInterface interface {
+	Run([]string)
+	Command() *commander.Command
 }
 
-func (c Command) Run(args []string) {
-	c.Fn(args)
+type BaseCommand struct {
+	UsageLine string
+	Short     string
+	Long      string
 }
 
-// List of all known commands.
-type CommandList []Command
-
-func (cl CommandList) PrintHelp() {
-	fmt.Fprintf(os.Stderr, "Usage: %s <COMMAND> [<ARGS>...]\n", filepath.Base(os.Args[0]))
-	fmt.Fprintf(os.Stderr, "Known commands:\n\n")
-	for _, cmd := range cl {
-		cmd.PrintHelp()
+func (b *BaseCommand) Command() *commander.Command {
+	return &commander.Command{
+		UsageLine: b.UsageLine,
+		Short:     b.Short,
+		Long:      b.Long,
 	}
 }
 
-func Run(cl CommandList) {
-	flag.Parse()
-
-	args := flag.Args()
-	if len(args) < 1 {
-		cl.PrintHelp()
-		return
-	}
-
-	cmdArg := args[0]
-	args = args[1:]
-	for _, cmd := range cl {
-		if cmd.Name == cmdArg {
-			if (cmd.MinArgs > 0 && len(args) < cmd.MinArgs) || (cmd.MaxArgs >= 0 && cmd.MaxArgs >= cmd.MinArgs && len(args) > cmd.MaxArgs) {
-				fmt.Fprintf(os.Stderr, "Wrong number of arguments!\n")
-				cmd.PrintHelp()
-			} else {
-				cmd.Run(args)
-			}
-			return
-		}
-	}
-	log.Fatalf("Unknown command '%s'", cmdArg)
+func (b *BaseCommand) Run(args []string) {
 }
 
 func getClient() *autoradio.RadioAPI {
@@ -72,42 +45,123 @@ func getClient() *autoradio.RadioAPI {
 	return autoradio.NewRadioAPI(etc)
 }
 
-func deleteMount(args []string) {
-	if err := getClient().DelMount(args[0]); err != nil {
-		log.Fatal(err)
-	}
+func generateUsername(path string) string {
+	return fmt.Sprintf("source%d", crc32.ChecksumIEEE([]byte(path)))
 }
 
-func listMounts(args []string) {
-	mounts, err := getClient().ListMounts()
-	if err != nil {
-		log.Fatal(err)
+func setRelay(m *autoradio.Mount, relayUrl string) {
+	if relayUrl == "" {
+		// Randomly generate source credentials.
+		m.Username = generateUsername(m.Name)
+		m.Password = autoradio.GeneratePassword()
+	} else {
+		// Validate the given relay URL.
+		u, err := url.Parse(relayUrl)
+		if err != nil {
+			log.Fatal(err)
+		}
+		m.RelayUrl = u.String()
+		m.Username = ""
+		m.Password = ""
 	}
-	for _, m := range mounts {
-		fmt.Printf("%s\n", m.Name)
+}
+
+// Edit a mountpoint.
+type editMountCommand struct {
+	BaseCommand
+	relay    string
+	fallback string
+}
+
+var UNSET = "UNSET"
+
+func newEditMountCommand() *editMountCommand {
+	return &editMountCommand{
+		BaseCommand: BaseCommand{
+			UsageLine: "edit-mount <path>",
+			Short:     "Edit an existing mountpoint",
+			Long: `
+Modify parameters of the specified mountpoint, such as the relay
+and the fallback URL. If the relay option is set, the mountpoint
+will not accept source connections anymore. To revert to the
+default, non-relay behavior, set the relay to the empty string
+(with --relay="").
+`,
+		},
 	}
 }
 
-func showMount(args []string) {
-	mount, err := getClient().GetMount(args[0])
+func (cmd *editMountCommand) AddFlags(f *gonutsflag.FlagSet) {
+	// Note that we use special values to figure out whether a
+	// flag has been specified or not. There might be better way
+	// to do this.
+	f.StringVar(&cmd.relay, "relay", UNSET, "Upstream URL to relay")
+	f.StringVar(&cmd.fallback, "fallback", UNSET, "Fallback stream URL")
+}
+
+func (cmd *editMountCommand) Run(args []string) {
+	if len(args) != 1 {
+		log.Fatal("Wrong number of arguments")
+	}
+
+	client := getClient()
+	mount, err := client.GetMount(args[0])
 	if err != nil {
 		log.Fatal(err)
 	}
 	if mount == nil {
 		log.Fatal("Mount not found")
 	}
+
+	if cmd.fallback != UNSET {
+		mount.Fallback = cmd.fallback
+	}
+	if cmd.relay != UNSET {
+		setRelay(mount, cmd.relay)
+	}
+
+	if err := client.SetMount(mount); err != nil {
+		log.Fatal(err)
+	}
+
 	fmt.Printf("%+v\n", mount)
 }
 
-func generateUsername(path string) string {
-	return fmt.Sprintf("source%d", crc32.ChecksumIEEE([]byte(path)))
+// Create a new mountpoint.
+type createMountCommand struct {
+	BaseCommand
+	relay    string
+	fallback string
+}
+
+func newCreateMountCommand() *createMountCommand {
+	return &createMountCommand{
+		BaseCommand: BaseCommand{
+			UsageLine: "create-mount <path>",
+			Short:     "Create a new mountpoint",
+			Long: `
+Create a new mountpoint at the specified PATH (which will become the
+absolute URL of the stream).
+`,
+		},
+	}
 }
 
-func doCreateMount(path, relayUrl string) {
-	if strings.Contains(path, "/") {
-		log.Fatal("Mount points should not contain a slash ('/').")
+func (cmd *createMountCommand) AddFlags(f *gonutsflag.FlagSet) {
+	f.StringVar(&cmd.relay, "relay", "", "Upstream URL to relay")
+	f.StringVar(&cmd.fallback, "fallback", "", "Fallback stream URL")
+}
+
+func (cmd *createMountCommand) Run(args []string) {
+	if len(args) != 1 {
+		log.Fatal("Wrong number of arguments")
+	}
+	fmt.Printf("args: %v\n", args)
+
+	path := args[0]
+	if !strings.HasPrefix(path, "/") {
+		path = "/" + path
 	}
-	path = "/" + path
 
 	// Check if the mount already exists.
 	client := getClient()
@@ -116,19 +170,11 @@ func doCreateMount(path, relayUrl string) {
 	}
 
 	// Create the new mount and set the relevant fields (depending
-	// on the relayUrl value).
+	// on the options passed to the command).
 	m := &autoradio.Mount{Name: path}
-	if relayUrl == "" {
-		// Randomly generate source credentials.
-		m.Username = generateUsername(path)
-		m.Password = autoradio.GeneratePassword()
-	} else {
-		// Validate the given relay URL.
-		u, err := url.Parse(relayUrl)
-		if err != nil {
-			log.Fatal(err)
-		}
-		m.RelayUrl = u.String()
+	setRelay(m, cmd.relay)
+	if cmd.fallback != "" {
+		m.Fallback = cmd.fallback
 	}
 
 	if err := client.SetMount(m); err != nil {
@@ -138,23 +184,132 @@ func doCreateMount(path, relayUrl string) {
 	fmt.Printf("%+v\n", m)
 }
 
-func createMount(args []string) {
-	doCreateMount(args[0], "")
+// Delete an existing mountpoint.
+type deleteMountCommand struct {
+	BaseCommand
+}
+
+func newDeleteMountCommand() *deleteMountCommand {
+	return &deleteMountCommand{
+		BaseCommand{
+			UsageLine: "delete-mount <path>",
+			Short:     "Delete a mountpoint",
+			Long: `
+Delete the specified mountpoint.
+`,
+		},
+	}
+}
+
+func (cmd *deleteMountCommand) Run(args []string) {
+	if len(args) != 1 {
+		log.Fatal("Wrong number of arguments")
+	}
+	if err := getClient().DelMount(args[0]); err != nil {
+		log.Fatal(err)
+	}
+	log.Printf("mountpoint %s removed", args[0])
+}
+
+// List known mountpoints.
+type listMountsCommand struct {
+	BaseCommand
+}
+
+func newListMountsCommand() *listMountsCommand {
+	return &listMountsCommand{
+		BaseCommand{
+			UsageLine: "list-mounts",
+			Short:     "List all configured mountpoints",
+			Long: `
+Outputs a list of all the currently configured mountpoints.
+`,
+		},
+	}
+}
+
+func (cmd *listMountsCommand) Run(args []string) {
+	if len(args) != 0 {
+		log.Fatal("Too many arguments")
+	}
+
+	mounts, err := getClient().ListMounts()
+	if err != nil {
+		log.Fatal(err)
+	}
+	for _, m := range mounts {
+		fmt.Println(m.Name)
+	}
+}
+
+// Show mountpoint information.
+type showMountCommand struct {
+	BaseCommand
+}
+
+func newShowMountCommand() *showMountCommand {
+	return &showMountCommand{
+		BaseCommand{
+			UsageLine: "show-mount <path>",
+			Short:     "Show mountpoint information",
+			Long: `
+Print information about the specified mountpoint (including
+the source credentials).
+`,
+		},
+	}
+}
+
+func (cmd *showMountCommand) Run(args []string) {
+	if len(args) != 1 {
+		log.Fatal("Wrong nubmer of arguments")
+	}
+	mount, err := getClient().GetMount(args[0])
+	if err != nil {
+		log.Fatal(err)
+	}
+	if mount == nil {
+		log.Fatal("Mount not found")
+	}
+	fmt.Printf("%+v\n", mount)
+}
+
+var cmdr = &commander.Command{
+	UsageLine: "radioctl <command> [<args>...]",
+	Short:     "Manage `autoradio' configuration",
 }
 
-func createRelay(args []string) {
-	doCreateMount(args[0], args[1])
+func addCommand(c CommandInterface) {
+	cmd := c.Command()
+	cmd.Run = func(unused *commander.Command, args []string) error {
+		c.Run(args)
+		return nil
+	}
+	if afc, ok := c.(HasAddFlags); ok {
+		afc.AddFlags(&cmd.Flag)
+	}
+	cmdr.Subcommands = append(cmdr.Subcommands, cmd)
+}
+
+func init() {
+	addCommand(newCreateMountCommand())
+	addCommand(newEditMountCommand())
+	addCommand(newDeleteMountCommand())
+	addCommand(newListMountsCommand())
+	addCommand(newShowMountCommand())
+	flag.Usage = usage
 }
 
-var commands = CommandList{
-	{"create-mount", "<path>", "Create a new mountpoint", createMount, 1, 1},
-	{"create-relay", "<path> <source_url>", "Create a new relay", createRelay, 2, 2},
-	{"delete-mount", "<path>", "Delete a mountpoint", deleteMount, 1, 1},
-	{"list-mounts", "", "List all known mountpoints", listMounts, 0, 0},
-	{"show-mount", "<path>", "Show configuration of a mount", showMount, 1, 1},
+func usage() {
+	fmt.Fprintf(os.Stderr, "Usage: %s\n\nGlobal options:\n", cmdr.UsageLine)
+	flag.PrintDefaults()
+	fmt.Fprintln(os.Stderr, "\nType \"radioctl help\" for more documentation.\n")
 }
 
 func main() {
+	flag.Parse()
 	log.SetFlags(0)
-	Run(commands)
+	if err := cmdr.Dispatch(flag.Args()); err != nil {
+		log.Fatal(err)
+	}
 }
diff --git a/debian/changelog b/debian/changelog
index 3fbd8b8a4e5c5811322d8b7a38c61d11471115da..20b64d50cdb2caaa2a4fe0da936846bc48c38171 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+autoradio (0.3.4) unstable; urgency=medium
+
+  * Updated radioctl.
+
+ -- ale <ale@incal.net>  Sun, 12 Oct 2014 12:54:11 +0100
+
 autoradio (0.3.3) unstable; urgency=medium
 
   * Serve 302 redirects for direct stream URLs instead of M3Us.