diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3174e7..e475040 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,9 @@
MumbleDJ Changelog
==================
+### June 21, 2016 -- `v3.0.1`
+* Added all strings that are output by commands to `config.yaml` for easier translation and tweaking.
+
### June 20, 2016 -- `v3.0.0`
* Significantly simplified installation process, now installable via `go install`.
* Commands may now have multiple aliases, configurable via config file.
diff --git a/bindata.go b/bindata.go
index 09a14e7..b9fe00e 100644
--- a/bindata.go
+++ b/bindata.go
@@ -68,7 +68,7 @@ func (fi bindataFileInfo) Sys() interface{} {
return nil
}
-var _configYaml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xac\x59\x6d\x6f\xdb\x38\x12\xfe\x9e\x5f\xc1\x3a\x07\x5c\x0b\x64\xdd\xa4\xb7\xdd\x5d\x04\x38\x14\x69\x9a\xbb\xe6\xd0\x5c\x8b\x26\xf7\xe1\x3e\x19\x8c\x44\x59\xdc\x48\xa4\x4e\xa4\xec\xf8\x7e\xfd\x3e\xc3\x37\xd9\x96\x12\xdb\xe9\x66\x17\xa8\x45\x0d\x67\x9e\x79\x1f\x52\xc7\xec\xa6\xab\xef\x2b\xf1\xe9\x5f\x47\xc7\xec\xe3\x8a\xdd\x70\x6b\x4b\x29\x3a\xf6\xcf\x56\x8a\xb9\x68\xb1\x7a\xa9\x9b\x55\x2b\xe7\xa5\x65\xaf\xb3\x37\xec\xdd\xe9\xd9\x2f\x03\x2a\xf6\xfa\xe6\xfa\x8e\x7d\x91\x99\x50\x46\xbc\xc1\x9e\x4c\xab\x42\xce\xa7\x2b\x5e\x57\x47\x47\xbc\x91\xb3\x07\xb1\x32\xe7\x47\x47\x0c\x7f\xc7\xec\xbf\xba\xbb\xeb\xee\x05\xbb\xf8\x76\xcd\xf0\x62\xea\x96\x57\xba\xb3\x58\x3c\x67\x93\x49\xa4\xbb\xd5\x9d\xca\x2f\x2b\xdd\xe5\x9b\xa4\xc7\xec\xdf\x5f\xef\xae\xce\xd9\x5d\x99\x78\x30\x69\x88\x43\xcb\xb2\x4a\x0a\x65\xd9\xf5\x27\x4f\x6a\x88\x45\x46\x2c\x3c\xe3\xa3\x5c\x14\xbc\xab\x6c\x0f\xe6\x93\x5f\x00\xe4\xba\xa6\x9d\x56\x33\x40\xe3\x4d\x03\x46\xb9\x7b\xd2\x76\x53\xec\x75\x41\xa2\x58\xae\x99\xd2\x96\x2d\x39\x36\xf1\xb4\xfd\x7e\xc5\x82\x88\x13\x66\x84\x63\x27\xea\xc6\xae\x98\xb1\xad\x54\x73\xf6\x7a\x32\x79\xe3\xd9\x85\x1d\xc0\xf5\x59\x54\x95\x7e\xc5\xae\x19\xaf\xc1\x89\xe4\xb1\xbb\x55\x23\xd8\xab\x52\x54\x0d\x2b\x74\x8b\xd5\x4a\x1a\xcb\x74\xe1\x76\x71\x95\x9b\xe9\x64\xa0\x40\xc9\x95\x12\x95\xa3\xb7\xb0\x0c\xf8\x38\xe9\xca\xc2\x41\x5d\xa3\x15\x79\x45\x89\xcc\x4a\xad\x46\x15\x5a\x4a\x53\x6e\xef\x0e\x5b\xe8\x27\xad\xb6\x5a\x27\x41\x3b\xf5\xf3\x64\xeb\x0e\xbd\xf4\xe0\x69\x53\x67\x04\xfd\xd3\x54\x7c\xc5\x78\x97\x4b\xcd\x0a\x59\x09\x33\x75\x4e\xb5\x4b\xcd\x4c\xd7\x34\xba\xb5\xf0\x41\x56\x6a\x44\x96\x61\xbc\x15\x6c\x52\x14\x75\x23\xe6\x13\x46\x6c\x26\x7c\x01\x7c\x8b\x89\x97\x47\xac\x44\x3b\x0b\x06\x3a\x4f\xa4\x70\xfa\xff\x3a\xd1\x89\xe4\xf1\xef\x1c\x26\x80\x3a\xdc\xb2\xba\x83\x55\xe1\xee\x1a\x9a\x40\x71\xf1\x98\x09\x91\x7b\xb7\x43\x9d\x39\x85\x36\xc7\x2f\x9e\x3d\x30\xf3\x20\x1b\x2f\xc8\x3d\xcf\xe8\x79\xd6\x12\xab\x73\x76\x3a\x7d\xff\x52\xe6\x84\xda\xf9\xb6\xe7\x1f\x97\x9e\x12\x71\xc3\x1f\x65\xdd\xd5\x01\x57\xde\x39\x0a\xc5\xa4\x82\x43\x60\x0f\xc4\x06\xbb\xf5\x9e\x39\x75\xee\xec\x54\x2b\xc8\x3b\x19\x19\x33\x92\x7b\x51\x35\x7f\x9c\x79\x75\xe2\x3a\x24\x8d\xca\x31\xac\x01\xde\x08\xed\x39\x09\x91\xc6\x6c\x89\x30\x33\x70\x98\xc5\xb7\xe7\xec\x7d\x12\x74\x6d\x98\x29\xbb\xa2\xa8\x28\x80\x84\xe2\xa8\x47\x39\x5b\x96\x42\xa5\x48\x34\x96\xb7\xd6\x7c\x70\xf4\xbc\xb3\xba\x06\xd6\x6c\xe6\x37\x89\x19\xa1\x2e\x78\x65\x44\x64\x78\xa1\x14\xf2\x3e\x13\xc1\x44\x52\x01\x64\xed\xad\x04\xbf\x38\xa6\x62\x2e\x95\x22\x79\xc8\x29\x1f\x7f\x84\xec\x1e\xe4\x41\x4a\x60\x31\x53\x62\x19\xf0\x9f\x83\x5d\x07\x19\x47\x7d\x1e\xa5\x98\xba\xc8\x73\x98\xc0\x78\xb0\xa5\xee\xaa\x1c\x92\x2c\x65\xc6\x66\x16\x79\x9b\x70\x4f\x8d\x18\x3d\x7b\xf7\xeb\xf4\x14\xff\x9d\xa5\x1c\xf9\x86\xa0\xdf\x93\x0d\xe5\x07\x78\xfc\xf2\xf3\xaf\x7f\xfb\xad\xdf\xcf\x8d\x59\xea\x36\x77\x8e\x89\x48\xa1\x27\xf6\x1b\xd1\x2e\x44\x3b\xc8\x7d\x05\xdd\xc3\xa6\x5d\x39\x1d\xe9\xd6\x93\xfa\x3f\x60\xab\x78\x2d\x9c\xc0\xd8\x4d\x3c\x79\x17\x5e\x81\x3c\xbe\xe8\x8b\xbb\x57\x2f\x7a\x38\xaa\xc9\x29\x90\x11\xc7\x1d\x92\xbd\xb7\xf3\x87\xe4\xd8\xb1\xb7\x28\xc5\x28\x0f\x54\x8c\xa1\x9e\x2c\x56\x8e\x69\x26\x5a\x2b\x0b\x99\x71\x2b\xc8\xc7\xb4\xe4\xf5\x27\x9c\x81\x1d\x58\x18\xc4\xa2\x50\xd9\x6a\xca\xae\x2d\x35\x91\x7b\x04\x32\x19\xa0\x12\x7c\x81\xf8\x29\xb1\xa4\xd5\x09\xbb\xef\x2c\xcb\xa5\xa1\xd0\x64\x12\x84\xbe\x62\x52\x45\x2a\xf9\x02\x36\x0a\x0c\xa5\x31\x1d\xa0\x6c\x9a\x9d\x47\xc1\x54\xc8\xb1\xa3\xed\x7c\xe4\xd5\xa8\xda\xb2\x21\x86\x0a\xf1\xad\xa8\xc4\x01\xe8\xa6\x05\xa3\xb6\x5b\x01\xfe\x0f\xd4\xca\x86\xdb\xd2\x45\xc5\x9a\xa2\x54\x43\xc7\xfc\xbb\x4d\x43\x9a\x2a\x57\x8d\x76\x56\x71\xe1\x62\x6c\xb2\x4b\x32\xf5\xe0\xa7\xa4\x87\xfe\xbc\x9f\x40\x10\xaf\xcb\xbb\xc8\x32\xca\x2b\xab\x1f\x30\x5b\xd0\x36\xa9\xa4\x95\xbc\x92\xff\x17\x29\x76\x96\xd2\x96\xc4\xb6\xe1\xa8\x61\x28\x1d\xe8\xc1\xae\x0b\x98\x31\x30\x7c\x83\x21\xf9\x63\x3f\x5c\x7e\xdf\xcc\xef\xdb\x98\x52\xb6\x02\x39\x96\x27\x5e\x55\xab\xf5\xec\x6d\x85\x6d\x57\xeb\x51\xbb\x1e\x1a\xbc\xa0\x2e\x8d\x08\xeb\x43\xc7\xc7\xbc\xdb\x35\x0b\x45\x31\x56\x20\x2f\xf8\xb3\x5e\xa2\xba\x2a\x84\xbb\xac\x85\x89\xf5\x62\x3b\xa1\x9c\xe4\xad\x36\xee\x85\xae\x0b\x08\xd4\x50\xec\xec\x74\xc0\x3f\xb4\x95\x6d\x09\x4b\x4e\x99\xa0\x7e\xba\x17\x76\x29\xc4\xfa\x78\x11\x74\x8d\x4c\xd7\x05\x49\x1a\x47\x16\x1c\x63\xc1\x7b\xaa\xa4\x3c\x2b\xfb\xc6\x7c\x49\x4f\x18\xd9\xd4\x1c\x8e\x31\x24\x67\xe5\x1c\x94\xeb\xa5\xaa\x34\x87\x93\x3c\xa7\x64\x8d\x8d\x9c\x48\xed\x4a\x5b\x5e\xf9\x28\x37\x14\x25\x34\x34\x39\xc6\xb9\x84\x21\xac\x06\x30\xb4\xca\x1b\xf9\x31\xf5\x27\xda\x36\x23\x5a\x80\x3a\x7b\x97\x0a\x29\x6a\x89\xce\x5d\xed\x80\x7d\x7d\x4b\x0f\x16\x10\x15\x6f\x0c\x35\x11\x94\x12\x41\x4e\x04\x64\x8a\xf0\x0c\x55\xa3\x45\x00\x16\xad\xae\x7d\x11\x22\xc1\x27\x24\x0f\x1b\xdb\x10\x8f\xe2\xb1\x01\x92\x19\x71\x3d\x67\xef\x7e\x7e\x42\x5e\xb4\xaa\x00\x0b\x8c\x40\x02\x7d\x2c\x94\x31\xaf\x4d\xe1\x46\x0a\xe2\x94\xa3\x22\x89\xda\x38\x31\xb5\x54\x9d\x15\x26\x4e\x60\xd8\xb5\x69\xf1\x30\x32\x26\x4b\x50\x57\xb0\xa4\x84\x63\x1a\x38\x4d\xd9\x95\x5a\xc8\x56\x2b\x37\xd1\x2e\x78\x2b\xc9\xde\x3e\x59\x5c\x05\xf4\x33\x32\xaa\x7b\xce\x4a\xd1\x86\x9c\x4f\xe6\x45\x72\xfc\xe5\xf3\xd7\x9b\xab\xb7\x53\xc7\xf4\x6d\xed\x2a\x5a\xfe\x3b\x4d\x62\x0b\x5d\x75\xb5\x18\x0c\xdf\x7e\x39\xf0\xf1\x6b\x34\xf2\x24\x5f\x7c\xd1\x4b\xaa\xcb\x9e\x8c\x21\xb3\xf0\x9c\x7b\xf2\xca\xbd\x22\xea\xd3\xb3\x14\xb9\x38\xae\x3c\x45\x5f\xfa\x77\xb4\xe1\x37\x00\xe2\x39\x4c\xd6\x9f\x06\xae\x5c\x68\x31\xbf\xfa\x61\xbb\x7c\xb8\x76\x80\xff\x43\xa5\x70\xe1\x77\xc2\x28\x45\xc2\xd4\x09\x33\x2a\x32\x8d\x78\x44\xd5\x0e\xa5\x88\x5e\x53\x23\x9c\x6e\xc6\xee\x7a\x26\x7f\x09\xc3\xbd\x13\xcb\xa8\x63\x6e\x97\x2e\xd7\x9b\x28\x8f\xe9\xcc\xe0\x86\xc8\x32\x4c\x32\x8e\x9a\x5c\xef\xc0\xb9\x51\xd2\x35\x19\x0c\x38\xe8\x6c\x14\x1d\xda\xcd\x51\x81\x5f\xa8\x37\x26\xcc\xa8\xb2\x6e\x34\x91\x19\x42\x4e\x1d\x34\x20\x0f\x50\xd2\x69\xc3\xed\x76\xa2\xce\xdd\x4f\xfa\xfb\x89\x4d\x6e\x3b\xcc\x74\x34\x00\x4c\xdc\x58\xe4\x89\xfb\x7c\x2e\x51\x90\x33\x77\xfc\x30\x7e\xee\xcd\x85\x91\x73\x45\xfd\x22\x12\xfb\x5c\x51\x34\x9f\x55\xcc\x8a\x47\x8c\xcd\x28\xb3\x7c\xbe\x6d\x81\xaf\x0a\xd5\x54\x2b\x41\xc7\x8a\xc0\xf4\x35\xa9\x5f\xc8\xd6\xd8\x37\x64\x1d\x92\x11\xa6\x94\x56\x14\xf2\x11\x61\xf8\x2a\x15\xe9\x8f\x02\x21\x40\x54\xfd\x49\xca\xa5\x52\x44\xd1\x9f\x36\xe0\xb1\xd4\x82\xd9\x95\x4b\xbe\xe0\xdb\x92\x9b\xc0\xcd\x96\xad\x10\xe1\x90\x8b\xb9\x99\x22\x46\x37\x54\xf8\x82\x75\x8e\x11\x72\x92\x1b\x18\x8b\x5d\x24\x79\xde\x51\xce\xea\x21\x4a\xa2\x55\xa2\xcd\xd7\x10\x4d\xd3\x40\x31\x73\x9e\xf0\xf1\xc2\xfe\x0e\x13\x50\x57\x71\xe1\xe9\xd8\x8c\xec\x3d\xf1\x81\x09\x62\x84\x9e\x33\xd9\x38\x5d\x94\x01\xa7\x64\xad\x6c\xfc\xf4\xff\xa9\x7f\xa0\x6a\xb7\x54\xe9\x44\x18\xcd\x90\x06\x73\x77\x3a\x8d\xab\x30\x6d\x0c\xfa\x34\xe3\xf6\xa1\x12\xcd\x91\x16\x42\xfc\x80\x68\x32\x58\xeb\x57\x7a\xed\x7d\xa5\x8f\xeb\x1b\x88\x27\x98\xbd\x4d\x3a\xa6\xe9\xfe\x90\xe2\x43\x8b\x23\xa0\x72\x89\x1a\x2d\xad\x88\xfd\xcf\x9d\x07\xe3\x09\x1a\x18\x14\xe2\x6e\x2f\xb0\x44\x38\x04\xac\xc6\x10\xbb\xfc\xfe\x61\xc0\xbe\x0d\x32\x92\xeb\x6a\xb3\x4f\xf4\x2d\x0d\x5c\x99\x75\xfd\x6b\xa7\x0e\x89\x74\xa0\x45\x66\x0e\xd4\xe2\x6b\x67\x9b\xce\x7a\x7c\x1b\xdd\xb6\xef\x51\xbe\xcf\xd2\xb4\x4c\xcf\xd2\x37\xc9\x50\x01\x13\xf8\xae\x6d\xd1\x66\x9c\x31\xf6\xc0\xbf\x46\x3d\x54\xc1\xbf\xa4\x6e\xfc\xd4\xbb\x43\x63\x2b\x2a\xb9\x71\x84\xbc\xd7\x9d\xaf\xbd\x81\x69\x3a\x66\xf6\xae\x21\x9d\x29\xf5\xc4\xa3\x3b\x09\x07\x5d\xc1\x03\x43\x24\x4e\xf5\xbb\x15\x4d\xa4\x03\x4d\x8a\x43\x1d\x75\x5d\xbb\x80\xb2\x02\x95\x83\x38\x9a\x21\xf6\x01\xc0\x74\x4c\xdf\x1f\x68\xdc\x32\x02\xb8\xf9\x53\x11\xa7\x4b\x88\x00\x9a\xea\xd0\x6e\x9c\x44\x35\x80\x56\xbe\x34\x1e\xfa\x66\x3b\x72\x27\xf7\xbb\x96\xaa\xde\x23\x19\x3d\xdd\x00\x14\x2d\x1f\x68\xb0\x1b\xbd\x40\x6f\x89\x7d\x0b\x91\x88\x42\xe7\xaf\x43\x83\xcd\xe2\xe5\xa0\x2c\xdc\x39\x99\x4c\x48\xe9\xe8\x6f\x3e\x68\x08\xd4\xb5\x70\xf1\x0a\xd5\xa3\x1a\x0f\xb2\xaa\x76\x2b\x41\x54\x03\x15\x1e\x0e\xc4\x7f\x6b\x75\x70\xb2\x3b\xba\xd0\x20\x85\x51\x1a\x2d\x4e\x5a\xb3\x3d\xbd\x47\x78\x64\xfd\x70\x31\xb3\x13\x64\x4f\x3b\x80\xea\xae\xaa\xe8\xc8\x31\xfa\x66\xb8\xf8\xd2\x88\xd9\x1c\x3c\xc2\x9d\x5a\x70\x4f\xb5\x1a\xad\xeb\x35\xbc\xba\x5b\x37\xa2\x1a\xc0\xac\x5f\x14\x40\xd1\x01\x2e\x7e\xe8\xc1\x47\x54\x8a\x9e\xa6\xd5\x0b\x49\xd3\xe7\x02\xfd\x89\xb7\xf3\x8e\x0e\x08\x11\x2d\xf5\xa8\x3d\xab\x78\x22\x1d\xe0\xa6\x37\xa3\xf5\x7b\xb3\xf3\xfe\x19\xc5\xdb\xf5\xd4\x7d\x2b\xb7\xea\x6a\x7f\x42\xda\x43\xb9\x48\x3a\xd4\x21\xfb\x81\x16\x0b\xb6\xf7\x74\x87\x54\xa4\xd8\xf1\x27\x36\xba\xfe\x92\xe6\xe1\xf9\x26\x8b\xcd\xfb\xa6\x4a\x22\x1d\xc2\xef\xea\xf1\x44\x79\x79\x5b\x1d\x57\xec\xd9\xa4\x68\x38\xc6\xe6\xdd\x6a\x38\xb2\x43\x61\x7d\xa3\x4d\x66\xeb\x3e\x38\x0a\x6e\x05\xdd\x41\xec\x96\xec\xe9\x06\x46\x6a\x0f\xf4\xfd\x77\xc7\x26\x74\xbe\x78\xd2\x70\x21\xec\xae\xd9\x12\x28\x9c\x45\xf7\xc1\x04\xb2\x21\xa4\x51\x03\x3d\x8b\x09\x6c\xcc\x5a\xae\xe0\xb0\xd4\x0a\x14\x20\xd7\x4b\xaa\x2a\x66\x90\xbb\x44\xe8\x11\x76\xfb\x34\x43\x4f\x77\xa8\xc7\xbe\xbb\x5d\x4f\xb9\x0c\x68\xe3\x57\xb6\x9d\xf2\x7b\xda\xe1\xec\xf8\xc4\xba\x39\x34\x9f\x6f\xa3\xf5\xe2\xd7\x42\x64\xae\xfb\x6c\x95\x87\x6a\xa4\x53\x0f\xff\xab\x49\xb7\xe7\x6e\x8c\x76\xcb\x49\x2f\xff\xe1\x63\x0f\xa5\x3c\xe1\x10\x39\xd6\x47\x16\x0f\x0d\x07\x34\x6a\x5d\x63\xea\x37\xfb\xf7\xb4\xfd\xa6\xdf\xd1\xc1\x77\x74\xee\x7d\x36\x9d\x2b\xee\xbe\x1b\xb2\x85\xf6\x67\x3f\x62\xfb\xf4\xf0\x7b\xd8\xdc\xfb\xec\xc8\x3b\x3e\xf1\xfe\x18\xd6\xed\xb1\xd7\xea\xf9\xbc\x12\x7b\x87\xc2\x06\xf9\x00\x70\xff\x76\xec\xd5\xf8\xfa\xc1\xf1\x72\xe7\x85\xf4\x57\xe4\xf1\xd3\x6a\xfa\xf8\xa7\xd5\x5b\x5d\x14\x51\xc3\x85\x68\x8d\xfb\xc8\xb6\x4b\xb7\x40\x38\x80\xb8\xf8\x91\xbe\x14\xed\x1e\x98\x6f\x7c\x9b\x89\x00\xc3\x6d\xe6\x4e\x7c\x8e\x6e\x08\x4f\x1f\x3c\x4d\x5e\x62\x12\x9b\x87\x7c\x0b\xb7\x9b\x68\xfc\x5c\xa5\x69\x8c\x9a\x7f\x1c\xd3\x4e\x98\x1e\xd3\xc7\x6f\xd3\x58\x6b\x97\x92\xc6\xfd\x3f\x02\x00\x00\xff\xff\xb4\xdb\xbe\x75\x05\x22\x00\x00")
+var _configYaml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xc4\x5a\xfb\x6f\x1b\x37\xf2\xff\xdd\x7f\x05\xab\x5c\x70\x09\xe0\x2a\x71\xae\x2f\x18\xb9\x14\xae\x93\xbb\xfa\x10\x37\x45\xe2\x16\xe8\x4f\x0b\x4a\xcb\x95\x58\xef\x92\x7b\xcb\x5d\xc9\xea\x5f\x7f\xf3\x20\xb9\x4f\x5b\x92\x13\xe0\xfc\xfd\xa2\x17\x71\x87\x33\xe4\x3c\x3f\x33\xbb\x4f\xc4\x75\x53\x2c\x72\xf5\xf6\x3f\x27\x4f\xc4\x4f\x3b\x71\x2d\xeb\x7a\xad\x55\x23\xfe\x5d\x69\xb5\x52\x15\xac\x5e\xda\x72\x57\xe9\xd5\xba\x16\xcf\x96\xcf\xc5\xab\x97\x67\xdf\x8d\xa8\xc4\xb3\xeb\xab\x1b\xf1\x5e\x2f\x95\x71\xea\x39\xec\x59\x5a\x93\xe9\xd5\x7c\x27\x8b\xfc\xe4\x44\x96\x3a\xb9\x55\x3b\x77\x7e\x72\x22\xe0\xef\x89\xf8\xc3\x36\x37\xcd\x42\x89\x8b\x5f\xaf\x04\x3c\x98\xd3\xf2\xce\x36\x35\x2c\x9e\x8b\xd9\x2c\xd0\x7d\xb2\x8d\x49\x2f\x73\xdb\xa4\x7d\xd2\x27\xe2\x97\x0f\x37\xef\xce\xc5\xcd\x3a\xf2\x10\xda\x21\x87\x4a\x2c\x73\xad\x4c\x2d\xae\xde\x32\xa9\x43\x16\x4b\x64\xc1\x8c\x4f\x52\x95\xc9\x26\xaf\xdb\xc3\xbc\xe5\x05\x38\x72\x51\xe0\xce\xda\x0a\x38\x9a\x2c\x4b\x60\x94\xd2\x2f\x5b\xf7\xc5\x5e\x65\x28\x4a\xa4\x56\x18\x5b\x8b\xad\x84\x4d\x32\x6e\x5f\xec\x84\x17\x71\x2a\x9c\x22\x76\xaa\x28\xeb\x9d\x70\x75\xa5\xcd\x4a\x3c\x9b\xcd\x9e\x33\x3b\xbf\x03\xce\xf5\xb3\xca\x73\xfb\x95\xb8\x12\xb2\x00\x4e\x28\x4f\xdc\xec\x4a\x25\xbe\x5a\xab\xbc\x14\x99\xad\x60\x35\xd7\xae\x16\x36\xa3\x5d\xd2\xa4\x6e\x3e\x1b\x5d\x60\x2d\x8d\x51\x39\xd1\xd7\xa0\x19\xe0\x43\xd2\x4d\x0d\x06\x6a\x4a\x6b\xd0\x2a\x46\x2d\x6b\x6d\xcd\xe4\x85\xb6\xda\xad\x87\xbb\xfd\x16\xfc\x27\xae\x56\xd6\x46\x41\x7b\xef\xc7\x64\x5d\x83\x5e\xf2\xe1\x71\x53\xe3\x14\xfe\x4f\x99\xcb\x9d\x90\x4d\xaa\xad\xc8\x74\xae\xdc\x9c\x8c\x5a\x6f\xad\x70\x4d\x59\xda\xaa\x06\x1b\x2c\xd7\x16\x3c\xcb\x09\x59\x29\x31\xcb\xb2\xa2\x54\xab\x99\x40\x36\x33\xb9\x81\xf3\x6d\x66\x2c\x0f\x59\xa9\x2a\xf1\x0a\x3a\x8f\xa4\x60\xf4\xff\x36\xaa\x51\xd1\xe2\x1f\x25\xa8\x00\xae\x23\x6b\x51\x34\xa0\x55\x30\x77\x01\x37\x81\x8b\xab\xbb\xa5\x52\x29\x9b\x1d\xae\xb3\x42\xd7\x96\xf0\x2f\xb9\xbc\x15\xee\x56\x97\x2c\x88\x7e\x27\xf8\x3b\xa9\x90\xd5\xb9\x78\x39\xff\xf6\xb1\xcc\xf1\xd4\x64\xdb\x96\x7f\x58\xba\x4f\xc4\xb5\xbc\xd3\x45\x53\xf8\x73\xa5\x0d\x51\x18\xa1\x0d\x18\x04\xf4\x01\xbe\x21\x3e\xb1\x65\x5e\x92\x39\x1b\x53\x29\xb4\xce\x12\x95\x19\xc8\x59\x54\x21\xef\x12\xbe\x4e\x58\x07\x49\x93\x72\x9c\x28\xe1\xbc\xe1\x68\x0f\x49\x08\x34\x6e\x20\xc2\x25\xc0\x21\x09\x4f\xcf\xc5\xb7\x51\xd0\x95\x13\x6e\xdd\x64\x59\x8e\x0e\xa4\x8c\x84\x7c\x94\x8a\xed\x5a\x99\xe8\x89\xae\x96\x55\xed\x7e\x24\x7a\xd9\xd4\xb6\x80\xb3\x2e\x13\xde\xa4\x12\x3c\x75\x26\x73\xa7\x02\xc3\x0b\x63\x20\xee\x97\xca\xab\x48\x1b\x38\x64\xc1\x5a\x02\xbb\x10\x53\xb5\xd2\xc6\xa0\x3c\x88\x29\xf6\x3f\x3c\xd9\x02\xc8\xbd\x14\xcf\x22\x31\x6a\xeb\xcf\x7f\x0e\xec\x1a\x90\x71\xd2\xc6\x51\xf4\xa9\x8b\x34\x05\x15\x38\x3e\xec\xda\x36\x79\x0a\x92\x6a\x8c\x8c\x7e\x14\xb1\x4e\x24\x53\x83\x8f\x9e\xbd\xfa\x7e\xfe\x12\xfe\xef\x2c\xc6\xc8\xaf\xe0\xf4\x07\xb2\xc1\xf8\x00\x1e\xdf\x7d\xf3\xfd\x3f\x7e\x68\xf7\x4b\xe7\xb6\xb6\x4a\xc9\x30\xe1\xa4\x70\x4f\xd8\xef\x54\xb5\x51\xd5\x28\xf6\x0d\xdc\xdd\x6f\xda\x17\xd3\x81\xae\x1b\xd4\xbf\x01\x5b\x23\x0b\x45\x02\x43\x35\x61\xf2\xc6\x3f\x02\xf2\xf0\xa0\x4d\xee\x7c\xbd\x60\xe1\x70\x4d\x89\x8e\x0c\x7e\xdc\x40\xb0\xb7\x7a\xfe\x31\x1a\x76\xea\x29\xa4\x62\x48\x0f\x98\x8c\xe1\x7a\x3a\xdb\x11\xd3\xa5\xaa\x6a\x9d\xe9\xa5\xac\x15\xda\x18\x97\xf8\xfe\x78\x4e\xcf\x0e\x58\x38\xf0\x45\x65\x96\xbb\xb9\xb8\xaa\xb1\x88\x2c\xc0\x91\x51\x01\xb9\x92\x1b\xf0\x9f\x35\x2c\x59\x73\x2a\x16\x4d\x2d\x52\xed\xd0\x35\x85\x06\x42\xce\x98\x98\x91\xd6\x72\x03\x3a\xf2\x0c\xb5\x73\x0d\x1c\xa5\xaf\x76\x19\x04\x63\x22\x87\x1d\x55\xc3\x9e\x57\x40\xd6\xd6\x25\x32\x34\xe0\xdf\x06\x53\x1c\x1c\xb4\xaf\xc1\x70\xdb\x81\x83\xff\x0b\x72\x65\x29\xeb\x35\x79\x45\xe7\xa2\x98\x43\xa7\xec\x3b\xa4\xc1\x9b\x1a\xca\x46\x7b\xb3\xb8\x22\x1f\x9b\xed\x93\x8c\x35\xf8\x3e\xe9\xbe\x3e\x1f\x26\x10\x88\xbb\xf2\x2e\x96\x4b\x8c\xab\xda\xde\x02\xb6\xc0\x6d\xda\xe8\x5a\xcb\x5c\xff\xa5\xa2\xef\x6c\x75\xbd\x46\xb6\xa5\x84\x1c\x06\xa9\x03\x6a\x30\x55\x01\x37\x75\x18\xd9\x63\x88\xf6\x38\xec\x5c\xbc\x2f\xe1\x7d\x3d\x94\x32\x70\xe4\x90\x9e\x64\x9e\xef\xba\xd1\x5b\xa9\xba\xda\x75\xbd\xb6\xeb\x1a\x32\xc3\x2a\x0d\x1e\xd6\xba\x0e\xfb\x3c\xed\x4a\x7c\x52\x0c\x19\x88\x05\xff\x6c\xb7\x90\x5d\x0d\xb8\xbb\x2e\x94\x0b\xf9\x62\x18\x50\x24\x79\x50\xc6\x59\x68\x57\x80\xa7\x86\x8b\x9d\xbd\x1c\xf1\xf7\x65\x65\x28\x61\x2b\x31\x12\xcc\xd7\x0b\x55\x6f\x95\xea\xc2\x0b\x7f\xd7\xc0\xb4\x2b\x48\x23\x1c\xd9\x48\x80\x05\xdf\x62\x26\x95\xcb\x75\x5b\x98\x2f\xf1\x17\x40\x36\xb3\x02\xc3\x38\x94\xb3\x23\x03\xa5\x76\x6b\x72\x2b\xc1\x48\xcc\x29\x6a\xa3\x17\x13\xb1\x5c\xd9\x5a\xe6\xec\xe5\x0e\xbd\x04\x41\x13\x31\x4e\x35\x28\xa2\xb6\x70\x30\x28\x95\xd7\xfa\xa7\x58\x9f\x70\x5b\x82\xb4\x70\xa8\xb3\x57\x31\x91\x42\x2e\xb1\x29\xe5\x0e\xd0\x2f\x97\x74\xaf\x01\x95\xcb\xd2\x61\x11\x81\x54\xa2\xd0\x88\x70\x64\xf4\xf0\x25\x64\x8d\x0a\x1c\x30\xab\x6c\xc1\x49\x08\x05\x9f\xa2\x3c\xd8\x58\x79\x7f\x54\x77\x25\x9c\x24\x41\xae\xe7\xe2\xd5\x37\xf7\xc8\x0b\x5a\x55\xc0\x02\x20\x90\x82\x3a\xe6\xd3\x18\xdf\x26\x23\x48\x81\x9c\x52\xc8\x48\xaa\x70\x24\xa6\xd0\xa6\xa9\x95\x0b\x08\x0c\x76\xf5\x35\xee\x21\x63\xd4\x04\x56\x85\x1a\x2f\x41\x4c\x3d\xa7\xb9\x78\x67\x36\xba\xb2\x86\x10\xed\x46\x56\x1a\xf5\xcd\xc1\x42\x19\x90\x31\x32\x64\xf7\x54\xac\x55\xe5\x63\x3e\xaa\x17\x82\xe3\x6f\x3f\x7f\xb8\x7e\xf7\x62\x4e\x4c\x5f\x14\x94\xd1\xd2\x3f\x11\x89\x6d\x6c\xde\x14\x6a\x04\xbe\x79\xd9\xf3\xe1\x35\x84\x3c\xd1\x16\xef\xed\x16\xf3\x32\x93\x09\x88\x2c\xf8\x9d\x32\x79\x4e\x8f\x90\xfa\xe5\x59\xf4\x5c\x68\x57\xee\xa3\x5f\xf3\x33\xdc\xf0\x03\x1c\x48\xa6\xa0\xb2\xb6\x1b\x78\x47\xae\x25\x78\xf5\xc7\x61\xfa\xa0\x72\x00\xff\xef\x33\x05\xb9\xdf\xa9\xc0\x10\xf1\xa8\x13\xd4\x68\x50\x35\xea\x0e\xb2\xb6\x4f\x45\xf8\x18\x0b\xe1\xbc\xef\xbb\xdd\x48\x7e\xef\xc1\x3d\x89\x15\x58\x31\x87\xa9\x8b\x6a\x13\xc6\x31\xf6\x0c\x04\x22\xd7\x1e\xc9\x10\x35\x9a\x9e\x0e\x47\x50\x92\x8a\x0c\x00\x1c\xa8\x6c\xe8\x1d\x96\x70\x94\xe7\xe7\xf3\x8d\xf3\x18\x55\x17\xa5\x45\x32\x87\x27\xc7\x0a\xea\x4f\xee\x8f\x12\xbb\x0d\xda\x4d\xa2\xce\xe9\x9f\xf8\xf7\xb5\x98\x7d\x6a\x00\xd3\x21\x00\x98\x11\x2c\x62\xe2\x36\x9e\xd7\x90\x90\x97\xd4\x7e\x38\xc6\xbd\xa9\x72\x7a\x65\xb0\x5e\x04\x62\x8e\x15\x83\xf8\x2c\x17\xb5\xba\x03\xd8\x0c\x69\x56\xae\x86\x1a\xf8\x60\x20\x9b\x5a\xa3\xb0\xad\xf0\x4c\x9f\xe1\xf5\x33\x5d\xb9\xfa\x39\x6a\x07\x65\x78\x94\x52\xa9\x4c\xdf\x81\x1b\x7e\xe5\x93\x34\x0a\xb3\x26\x09\x9c\xdb\x2b\x18\x1b\xb0\xa9\xaa\x2a\x5b\xc1\x96\x1b\x74\x68\x2e\x0b\x36\x20\x5f\xcd\x40\x94\xba\x08\xe8\xbb\xc2\x66\xf4\x6e\xc8\xd3\x89\xc7\x05\x69\xe4\x71\xc9\x0f\x28\x23\x34\x55\x05\x41\x04\x67\x0f\x54\x6d\xe3\xf6\x93\x02\xb7\x44\xa2\xb6\xbb\xa3\xf0\x0e\x9a\x69\x3b\x20\xf0\xa2\x08\x0b\xc4\x3b\x4a\x08\xde\xdf\xd6\xd2\x79\x6e\xf5\xba\x52\xca\x37\xde\x80\xe5\xd1\x8b\x6d\x89\xc9\xd8\x5f\xf7\x09\x84\x81\x96\x0e\x6e\x2f\x2e\xa2\x3c\x76\x1e\xf2\x04\xef\xb9\xc1\x52\xc1\x0f\x3a\x27\x9a\x47\x90\x93\x90\x77\xb0\x0f\x8b\x7f\x82\x59\xb0\xd2\x51\xc8\x10\x9b\x89\xbd\xa7\x1c\x2c\x40\x0c\xe1\x40\x66\x9c\xa6\x0b\x32\xc0\x51\x96\x95\x2e\xb9\x23\x79\xdb\xfe\xc0\x0c\xbc\x35\xb1\x4b\x0d\x6a\x88\xcd\x02\x75\xcc\x61\x15\x54\x1b\x02\x31\xf0\x8d\x2e\x20\x7e\x87\xb4\x66\x1b\x17\x57\x7c\xcf\x06\xed\xe8\x02\x0b\x24\x36\xf5\x68\x99\xae\x4b\x76\xf2\xba\x3f\x2d\x82\xdf\xac\xf1\x3d\x77\x25\x8d\xcb\x65\xdd\xa2\xc1\xf6\x8f\xd1\x04\xe1\x17\x0b\xfb\x2b\x91\x4b\xb3\x6a\xc8\xcb\xc5\x5b\x8b\x2e\x0e\x01\x5b\x58\x40\x9c\x91\x12\x4f\x43\x5d\x0a\xc1\x1b\x31\x7b\x3a\x13\xcf\x5c\x03\xa6\x87\x63\xcd\x9e\xba\xd9\x29\xfc\x37\x85\xff\xaa\x7a\x39\x7f\x3e\x12\x18\xca\xa7\x6b\x16\xae\xd6\x35\xe5\x22\xe2\x03\xe0\x93\xca\x4b\x2a\x6b\x39\x17\x1f\x51\x28\xc1\x54\xc8\x89\xad\xf0\xad\xce\x73\xb0\x10\xf5\xe8\xed\x2c\xa0\xd0\x6e\xa1\x00\xee\xaa\xd8\xc4\xb4\x81\x14\x7c\xeb\xa4\x73\x06\x4c\x10\x40\x34\x1b\xad\xb5\x2b\xad\x2b\x71\x29\x0f\xeb\x3d\xf3\xcf\xa0\xb9\x72\xb1\x0f\xb7\x6d\x17\xca\xf6\x90\x60\x9e\x54\x43\x11\xd6\xb5\x0a\x00\x67\x18\xaa\xe3\xc8\xf7\xd1\xdf\x54\x79\x0c\xdb\x0b\xf1\xdb\xc7\xf7\xb1\x6b\xc7\xe8\xa3\x11\x10\xa9\x0d\x99\xc2\x5d\xa2\xe1\x67\x43\x46\x50\x60\x75\x3a\x4c\x26\xbf\x58\x41\xeb\x21\x91\x6c\x31\xb7\x64\x38\x92\x6a\xb9\x96\x15\x58\x00\x33\x3a\x08\x7f\xe6\x9e\x0f\x38\x7b\x86\xb5\xb5\x49\x0e\x20\x23\x72\xfe\x03\x67\x5d\xf4\x10\xf6\x30\x5f\xa5\xc9\xb3\x80\x54\x20\x29\x75\x1b\x10\x63\xb8\x41\xd8\x25\x25\x22\x0c\x14\xc4\x45\x20\x13\xc1\xac\x37\x7c\x31\x17\xbf\xd8\x96\x19\x5a\x18\x14\x00\x01\x05\x17\x56\xc3\xab\x42\xec\xfa\x89\x01\x3d\x85\xa3\xbc\x5e\xbc\x79\xea\x5e\xbf\x58\xbc\x61\x7a\x01\xbf\xcf\xe8\x27\xdb\xab\x6b\x91\xf3\xd7\x8b\xea\xcd\x6b\x4d\xf4\xfa\x0d\x9b\x0f\x5c\xb9\x27\x00\x11\x67\xd0\xe3\x03\x22\x9e\xa6\xad\x0c\x77\x9f\xd9\xc9\x36\x80\xed\x06\x5a\x24\x8e\x70\x90\x21\x97\x25\x81\x3b\xac\x82\x0b\xe5\x25\xa5\x0d\xf9\x94\xd7\x62\x05\x0f\x62\x58\x30\x36\x0d\xea\x0e\x69\x1d\xb6\x19\x48\x19\x07\x45\x06\x12\x8e\xa3\xc3\x4c\x85\x07\xa1\x85\xcf\x8e\x0e\xce\x0a\x02\xe5\x12\xd2\xbb\xaf\xb2\x3d\x09\xd7\xc0\x72\xc0\x7b\x62\x9a\x04\x78\xa6\x8d\xe2\xf1\x02\x50\xcd\x7d\x85\x45\xa4\x47\x10\x7a\xef\xc5\x23\xe9\xe8\xea\x4b\x77\xe4\xd5\x3f\x34\x75\xd9\xd4\x7c\xc0\x1e\xe0\x6f\x61\x32\x43\x7d\x6c\xd8\x97\x6d\x55\xf6\x20\x6c\x6f\x82\xf0\xd5\xdb\xf7\x06\x88\x0d\xc2\xd2\x94\x24\x47\x7e\x39\x7f\xb5\x41\x89\xe8\x57\xc1\x27\xfc\x1e\xb2\xd0\x01\xfa\xe9\x50\x8f\x55\xc4\x0f\xb1\xe1\xb8\xef\xd9\xb1\xd9\x35\x28\xb1\x37\x25\x5b\xd8\x86\xe1\x65\xb8\x6f\x98\xa4\xb5\xfe\x82\x3a\xc5\x4a\xae\xee\x68\xd8\x77\xa8\x2e\x59\x0b\x7d\x65\x7a\xe6\xa0\xc0\x90\x1b\x4e\x7d\xfc\x01\xf2\x89\xc1\x1f\xd4\x09\xc7\x84\xec\x75\xab\xcb\xfd\xba\x8c\xa4\x23\x65\x65\xc7\xfa\xda\x55\x41\x81\x54\x2b\xc0\x3a\xc8\xd1\x8d\xd5\xb3\x57\x07\xed\xe4\xb8\xa4\xbc\x36\xd6\x01\xe0\x39\xce\xbd\x78\x72\xbd\xf0\xb2\xca\x7d\x9a\x88\x53\xd5\xc3\x35\x12\xb6\x4c\x68\xa6\xfc\xa2\xaa\x89\x33\xe3\x03\xca\x71\x1c\x7d\x77\xe0\xf8\xd8\x4b\x30\x43\x97\xb2\xe2\x96\x69\x8a\x3f\xfe\xf5\xa6\xe8\x63\x75\xc7\x2c\x79\x9c\xc6\x11\x5f\xee\x57\x32\x52\x8d\xf4\xba\x7e\x6c\x60\xb6\x8d\x5d\xff\xfd\xcf\xc3\xda\x0c\x84\xc9\x5a\xc9\x54\x55\x6d\xcd\xbb\x0c\x6d\x1a\xde\x0b\xd7\xfa\x27\xa5\x83\x25\xf7\xee\xbe\xa0\xbe\x70\x82\x07\x31\xf9\xd3\x6a\x53\x1c\x50\x03\x98\x6e\xa4\x22\x5c\x3e\xd2\xf7\xae\x01\x34\xbb\xd8\x1d\x41\x82\x82\x6a\xcd\x2f\x02\xbd\xa1\xc3\x6b\x31\x9d\xb1\xdf\x80\xdd\xb1\x0a\xf0\xcc\x1f\xc7\x1f\xb6\x50\x94\xc6\xc0\x10\x7b\x95\x4a\xe0\x1d\x8e\x55\xa9\x24\xa7\xc1\xb1\xee\x60\x32\xec\x83\xa9\x59\x94\x86\x41\x7e\x10\x8d\x38\x21\x92\x13\x92\x1e\x82\x14\xd0\x38\x1e\x3a\x69\xdf\x99\xd1\xcb\x40\x83\xfd\xa1\xf1\xf7\xe1\x47\xa1\xa5\xbd\x05\x88\xbe\x5f\xcf\x48\x35\xd2\xf2\xed\x91\x2a\xfe\x54\x5b\x1f\xd2\x34\x57\xc4\x29\x47\xae\xa0\xd7\x01\x08\xe1\x86\xa3\xb5\x10\x27\x78\x5d\xff\xd6\x64\xef\x21\x5b\xda\xd1\x51\xe9\x3d\x12\xce\x03\x27\x9f\x8c\x17\x1f\x1b\x62\xfd\x0e\x3c\xc0\xc1\xd8\xbb\xdf\x03\x93\xa6\x7d\x44\x1b\xee\x05\x70\xee\xb6\x52\x55\xdb\x5e\x98\xf0\x48\xf8\x47\x62\x2b\x5d\xec\x33\xa6\x80\x3f\x39\x99\xf6\x80\xd5\x83\xd5\xf3\x3d\x45\xb2\x13\x8d\xd8\x50\xee\x57\x3f\x52\x8d\x34\x59\x3c\x2a\x0c\x83\x8f\x50\x14\xe2\x0f\x8e\xcb\x18\x08\xb1\xd7\xd9\x00\x22\x95\xd5\xaa\xc1\x01\xe3\x21\x75\xc1\x33\x48\x02\x83\x4e\xcf\x06\xe7\x00\x15\x31\x6c\x09\x72\x46\x3d\x1c\xc6\x9c\xdd\xc4\x6e\x76\xa0\xeb\xc0\x1d\x5f\x23\x01\x42\x21\x40\xd3\xab\x40\xf1\xdc\x41\x40\x7c\xe1\x44\xb4\x03\x76\x28\x29\x81\x56\x1d\x5b\xac\xac\xc9\xb9\x5b\xe3\xb6\xaa\x5d\x05\xaf\x42\xba\xb4\xdb\x60\x77\xaa\x8d\x60\x0b\x22\x06\x3f\x10\x36\x46\xd2\x91\x2d\xf1\xc9\x24\x60\xec\xf7\x1f\x5f\x02\x2d\x52\xcf\xf0\x65\xa1\x62\x82\xd3\xa5\x87\xf1\x00\xca\xa1\x19\xd4\x58\xf2\xb0\x19\x84\xf3\xf5\x10\x68\xf7\xc0\x07\xc2\x4f\x68\x28\x79\x58\x7e\x80\x4d\x02\xe9\x58\xf5\xcb\xcf\x68\x75\x80\xed\x02\x5f\x27\x66\x31\x53\xf1\xf0\x1e\xdf\x84\x6a\x77\xfb\xc8\x66\x07\x1b\x65\x7f\xb1\xee\x18\xb4\xcd\x82\x6d\xbf\x4c\x6f\x09\xf8\xc5\x41\x1a\xd4\x4d\x5b\x3b\x3a\x3a\x34\xfb\x47\xd2\xb1\x8e\x9a\x62\x3a\xf7\x3f\xbe\xc7\x99\xd6\xde\xe3\xf2\x7c\x9c\x84\x44\x75\xf5\xe6\xbd\x83\x31\xc8\x03\x4e\x59\xe6\x4d\x25\xf3\xf8\x91\xc1\x1e\xdd\x4f\xcf\xa4\x89\x61\x89\xa3\x89\xfd\x1a\x27\xb2\x63\x35\xf8\xab\xa4\x51\x40\xff\x53\x89\x43\x52\x37\xed\x88\xf1\xfb\xce\x0f\xa9\x70\x9c\x46\xac\x70\xfc\x9d\x57\x00\x32\x77\x7c\xfc\xf4\x54\xf0\x6c\xf7\xd0\x29\x7c\xbc\x78\x7f\x50\x84\xb0\x9e\x97\xc7\x67\xa6\xbd\x95\xc2\x57\x8a\xfb\xb5\xc5\x74\x23\x1f\xac\x8e\x8c\xdf\x8f\xc4\xc6\x77\x46\x61\x48\x4f\xd9\x93\xde\x9a\xef\x53\x24\x9f\xa2\xed\x62\x46\x1c\xda\x3e\xa6\x57\x63\xc2\xbe\xf6\xd6\x4e\x1d\xd0\x25\x12\xd9\xf8\xce\x93\x5e\xf3\xe0\xa5\x81\x8d\xeb\xd4\x81\xc5\x8e\xe7\xdc\x84\xc0\xf3\x3c\x54\x07\x7a\xe9\xb8\x4f\x05\x44\x9b\xf0\x05\x86\x96\xa6\xd5\x71\x40\xc0\x72\x73\x48\x3b\xc2\x74\xc7\x86\xc4\x47\xda\x75\x74\x4c\x1c\x11\x10\xdc\xab\x3c\x26\x22\xf8\x46\xe3\x90\xf0\xeb\xf7\xc4\x04\x28\x31\x7c\x95\xb8\x57\x67\x2d\xed\x78\x10\x75\xcf\xba\x3b\xb6\xe8\x7d\x0a\xde\x13\xbe\xae\x84\xf2\x46\x9f\xf9\xa5\xbe\x70\xdb\xd8\xf9\xfd\xdd\xc5\xaf\x8d\x68\xe6\x47\xcb\x07\x35\xc9\x88\x34\xf8\xe5\x4b\x1b\x5d\x2c\xad\xfb\x2d\xe4\x7d\xe1\x45\xfb\x86\x78\xd2\x73\x45\xb4\xb8\x7a\x04\x57\xbf\x2f\x4c\xb5\x33\x8b\xef\xaf\xa9\x0d\xc0\x61\x39\x5b\x8a\x3f\x7d\x3b\xc0\x4c\x4c\x38\xb6\x05\xac\x4f\x2c\x1e\x1b\xe0\xd0\x0d\xda\x42\xff\xe5\xb1\xff\xe7\x15\x54\xc0\xd3\x89\x32\xb6\x59\xad\x1f\x7a\x29\x0b\x98\x9b\x68\xa6\x82\xa0\xfb\xe2\x52\x06\x1d\x0d\x8c\xe3\x57\x83\x55\x38\x10\x78\x77\x6b\x0d\x4f\x13\xe3\xe2\xa0\x61\xe3\xe4\x9c\x71\x72\xcc\xf8\x60\xa1\xcd\x25\x7d\xec\x2a\x36\x96\xdf\x67\x21\xdb\x47\xcc\x1a\x7d\x12\x49\x90\x4d\xda\x7d\x6f\xc4\x9d\x48\xc8\x31\xf4\xb8\x23\x06\xe1\xf4\x80\x3f\xfe\x11\xd9\x28\x9b\x0c\x37\x8f\xcf\x18\xd5\x77\xf8\x84\xf2\xc1\xe1\xe4\xf4\x6c\xf2\xf3\xf4\xf9\x7f\x1a\x50\x3e\xde\x40\xf7\x30\x3c\xd6\x46\x1d\x36\xc4\xa7\xb6\xab\x55\xae\x0e\xce\x2c\x3d\xf2\x91\xa1\xda\xa7\x53\x8f\xa6\xd7\x8f\x4e\x3f\x37\x2c\xa4\xfd\xe6\x2e\x7c\xab\x1d\xbf\x26\xb6\xe6\x85\xcd\xb2\xfd\x83\x79\x62\x94\x26\x40\x8b\x43\x85\xc8\xae\x65\x14\x73\x83\x27\x15\x7d\xb6\x3d\x26\xe6\x60\x1e\x06\x75\x4f\x4c\x36\xaa\x72\xf4\x41\xf1\x3e\xb5\x7b\xc2\x91\xf6\x36\x9f\xd3\x13\x05\x97\xf0\xcc\x7b\xdf\xa1\xee\xd3\x5d\x38\x79\xfb\x8d\x6f\xbb\x14\x5d\xd1\xbb\x58\xf8\xfc\x6b\xef\x25\x89\x6e\x7c\x47\x7b\xf4\x84\xef\x92\x6a\x29\xdf\xd2\x7f\x0e\x06\xed\xb1\x34\x71\xfc\x84\xf1\x1a\xe6\x3b\x80\xb4\xa6\x94\xc2\xdb\x68\xa6\xbb\xd5\x07\x4c\x89\x21\xf4\x5d\x77\x30\x8c\x39\xa2\x52\x50\x63\x1c\x86\xa2\x67\xd7\x7b\xd9\x8c\x3b\xc6\xaf\xda\x9b\x1a\x1c\x32\xa9\xf0\x02\x91\xd7\xef\xb4\xdb\xc5\x11\x57\xf8\x4c\x90\xee\x27\x73\xfc\xfa\x98\xdf\x42\x66\xfc\xbe\xdc\xa4\xdd\xdf\x43\xa0\xe2\x07\x2d\xde\x2c\xfd\x6c\x16\xb4\xe5\x1e\x60\xc0\x34\x1d\xa0\xd3\xcf\x3d\x11\xc8\xb4\xca\xf7\x63\xae\xc8\xee\x7f\x01\x00\x00\xff\xff\x68\xa7\xcd\x9c\x81\x34\x00\x00")
func configYamlBytes() ([]byte, error) {
return bindataRead(
@@ -83,7 +83,7 @@ func configYaml() (*asset, error) {
return nil, err
}
- info := bindataFileInfo{name: "config.yaml", size: 8709, mode: os.FileMode(420), modTime: time.Unix(1466393994, 0)}
+ info := bindataFileInfo{name: "config.yaml", size: 13441, mode: os.FileMode(420), modTime: time.Unix(1466549845, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
diff --git a/bot/config.go b/bot/config.go
index 57fcb9d..d74b117 100644
--- a/bot/config.go
+++ b/bot/config.go
@@ -65,9 +65,19 @@ func SetDefaultConfig() {
viper.SetDefault("admins.names", []string{"SuperUser"})
// Command defaults.
+ viper.SetDefault("commands.prefix", "!")
+ viper.SetDefault("commands.common_messages.no_tracks_error", "There are no tracks in the queue.")
+ viper.SetDefault("commands.common_messages.caching_disabled_error", "Caching is currently disabled.")
+
viper.SetDefault("commands.add.aliases", []string{"add", "a"})
viper.SetDefault("commands.add.is_admin", false)
viper.SetDefault("commands.add.description", "Adds a track or playlist from a media site to the queue.")
+ viper.SetDefault("commands.add.messages.no_url_error", "A URL must be supplied with the add command.")
+ viper.SetDefault("commands.add.messages.no_valid_tracks_error", "No valid tracks were found with the provided URL(s).")
+ viper.SetDefault("commands.add.messages.tracks_too_long_error", "Your track(s) were either too long or an error occurred while processing them. No track(s) have been added.")
+ viper.SetDefault("commands.add.messages.one_track_added", "%s added 1 track to the queue:
%s from %s")
+ viper.SetDefault("commands.add.messages.many_tracks_added", "%s added %d tracks to the queue.")
+ viper.SetDefault("commands.add.messages.num_tracks_too_long", "
%d tracks could not be added due to error or because they are too long.")
viper.SetDefault("commands.addnext.aliases", []string{"addnext", "an"})
viper.SetDefault("commands.addnext.is_admin", true)
@@ -76,26 +86,35 @@ func SetDefaultConfig() {
viper.SetDefault("commands.cachesize.aliases", []string{"cachesize", "cs"})
viper.SetDefault("commands.cachesize.is_admin", true)
viper.SetDefault("commands.cachesize.description", "Outputs the file size of the cache in MiB if caching is enabled.")
+ viper.SetDefault("commands.cachesize.messages.current_size", "The current size of the cache is %.2v MiB.")
viper.SetDefault("commands.currenttrack.aliases", []string{"currenttrack", "currentsong", "current"})
viper.SetDefault("commands.currenttrack.is_admin", false)
viper.SetDefault("commands.currenttrack.description", "Outputs information about the current track in the queue if one exists.")
+ viper.SetDefault("commands.currenttrack.messages.current_track", "The current track is %s, added by %s.")
viper.SetDefault("commands.forceskip.aliases", []string{"forceskip", "fs"})
viper.SetDefault("commands.forceskip.is_admin", true)
viper.SetDefault("commands.forceskip.description", "Immediately skips the current track.")
+ viper.SetDefault("commands.forceskip.messages.track_skipped", "The current track has been forcibly skipped by %s.")
viper.SetDefault("commands.forceskipplaylist.aliases", []string{"forceskipplaylist", "fsp"})
viper.SetDefault("commands.forceskipplaylist.is_admin", true)
viper.SetDefault("commands.forceskipplaylist.description", "Immediately skips the current playlist.")
+ viper.SetDefault("commands.forceskipplaylist.messages.no_playlist_error", "The current track is not part of a playlist.")
+ viper.SetDefault("commands.forceskipplaylist.messages.playlist_skipped", "The current playlist has been forcibly skipped by %s.")
viper.SetDefault("commands.help.aliases", []string{"help", "h"})
viper.SetDefault("commands.help.is_admin", false)
viper.SetDefault("commands.help.description", "Outputs this list of commands.")
+ viper.SetDefault("commands.help.messages.commands_header", "
Commands:
")
+ viper.SetDefault("commands.help.messages.admin_commands.header", "
Admin Commands:
")
viper.SetDefault("commands.joinme.aliases", []string{"joinme", "join"})
viper.SetDefault("commands.joinme.is_admin", true)
viper.SetDefault("commands.joinme.description", "Moves MumbleDJ into your current channel if not playing audio to someone else.")
+ viper.SetDefault("commands.joinme.messages.others_are_listening_error", "Users in another channel are listening to me.")
+ viper.SetDefault("commands.joinme.messages.in_your_channel", "I am now in your channel!")
viper.SetDefault("commands.kill.aliases", []string{"kill", "k"})
viper.SetDefault("commands.kill.is_admin", true)
@@ -104,66 +123,98 @@ func SetDefaultConfig() {
viper.SetDefault("commands.listtracks.aliases", []string{"listtracks", "listsongs", "list", "l"})
viper.SetDefault("commands.listtracks.is_admin", false)
viper.SetDefault("commands.listtracks.description", "Outputs a list of the tracks currently in the queue.")
+ viper.SetDefault("commands.listtracks.messages.invalid_integer_error", "An invalid integer was supplied.")
+ viper.SetDefault("commands.listtracks.messages.track_listing", "%d: %s, added by %s.
")
viper.SetDefault("commands.move.aliases", []string{"move", "m"})
viper.SetDefault("commands.move.is_admin", true)
viper.SetDefault("commands.move.description", "Moves the bot into the Mumble channel provided via argument.")
+ viper.SetDefault("commands.move.messages.no_channel_provided_error", "A destination channel must be supplied to move the bot.")
+ viper.SetDefault("commands.move.messages.channel_doesnt_exist_error", "The provided channel does not exist.")
+ viper.SetDefault("commands.move.messages.move_successful", "You have successfully moved the bot to %s.")
viper.SetDefault("commands.nexttrack.aliases", []string{"nexttrack", "nextsong", "next"})
viper.SetDefault("commands.nexttrack.is_admin", false)
viper.SetDefault("commands.nexttrack.description", "Outputs information about the next track in the queue if one exists.")
+ viper.SetDefault("commands.nexttrack.messages.current_track_only_error", "The current track is the only track in the queue.")
+ viper.SetDefault("commands.nexttrack.messages.next_track", "The next track is %s, added by %s.")
viper.SetDefault("commands.numcached.aliases", []string{"numcached", "nc"})
viper.SetDefault("commands.numcached.is_admin", true)
viper.SetDefault("commands.numcached.description", "Outputs the number of tracks cached on disk if caching is enabled.")
+ viper.SetDefault("commands.numcached.messages.num_cached", "There are currently %d items stored in the cache.")
viper.SetDefault("commands.numtracks.aliases", []string{"numtracks", "numsongs", "nt"})
viper.SetDefault("commands.numtracks.is_admin", false)
viper.SetDefault("commands.numtracks.description", "Outputs the number of tracks currently in the queue.")
+ viper.SetDefault("commands.numtracks.messages.one_track", "There is currently 1 track in the queue.")
+ viper.SetDefault("commands.numtracks.messages.plural_tracks", "There are currently %d tracks in the queue.")
viper.SetDefault("commands.pause.aliases", []string{"pause"})
viper.SetDefault("commands.pause.is_admin", false)
viper.SetDefault("commands.pause.description", "Pauses audio playback.")
+ viper.SetDefault("commands.pause.messages.no_audio_error", "Either the audio is already paused, or there are no tracks in the queue.")
+ viper.SetDefault("commands.pause.messages.paused", "%s has paused audio playback.")
viper.SetDefault("commands.reload.aliases", []string{"reload", "r"})
viper.SetDefault("commands.reload.is_admin", true)
viper.SetDefault("commands.reload.description", "Reloads the configuration file.")
+ viper.SetDefault("commands.reload.messages.reloaded", "The configuration file has been successfully reloaded.")
viper.SetDefault("commands.reset.aliases", []string{"reset", "re"})
viper.SetDefault("commands.reset.is_admin", true)
viper.SetDefault("commands.reset.description", "Resets the queue by removing all queue items.")
+ viper.SetDefault("commands.reset.messages.queue_reset", "%s has reset the queue.")
viper.SetDefault("commands.resume.aliases", []string{"resume"})
viper.SetDefault("commands.resume.is_admin", false)
viper.SetDefault("commands.resume.description", "Resumes audio playback.")
+ viper.SetDefault("commands.resume.messages.audio_error", "Either the audio is already playing, or there are no tracks in the queue.")
+ viper.SetDefault("commands.resume.messages.resumed", "%s has resumed audio playback.")
viper.SetDefault("commands.setcomment.aliases", []string{"setcomment", "comment", "sc"})
viper.SetDefault("commands.setcomment.is_admin", true)
viper.SetDefault("commands.setcomment.description", "Sets the comment displayed next to MumbleDJ's username in Mumble.")
+ viper.SetDefault("commands.setcomment.messages.comment_removed", "The comment for the bot has been successfully removed.")
+ viper.SetDefault("commands.setcomment.messages.comment_changed", "The comment for the bot has been successfully changed to the following: %s")
viper.SetDefault("commands.shuffle.aliases", []string{"shuffle", "shuf", "sh"})
viper.SetDefault("commands.shuffle.is_admin", true)
viper.SetDefault("commands.shuffle.description", "Randomizes the tracks currently in the queue.")
+ viper.SetDefault("commands.shuffle.messages.not_enough_tracks_error", "There are not enough tracks in the queue to execute a shuffle.")
+ viper.SetDefault("commands.shuffle.messages.shuffled", "The audio queue has been shuffled.")
viper.SetDefault("commands.skip.aliases", []string{"skip", "s"})
viper.SetDefault("commands.skip.is_admin", false)
viper.SetDefault("commands.skip.description", "Places a vote to skip the current track.")
+ viper.SetDefault("commands.skip.messages.already_voted_error", "You have already voted to skip this track.")
+ viper.SetDefault("commands.skip.messages.voted", "%s has voted to skip the current track.")
viper.SetDefault("commands.skipplaylist.aliases", []string{"skipplaylist", "sp"})
viper.SetDefault("commands.skipplaylist.is_admin", false)
viper.SetDefault("commands.skipplaylist.description", "Places a vote to skip the current playlist.")
+ viper.SetDefault("commands.skipplaylist.messages.no_playlist_error", "The current track is not part of a playlist.")
+ viper.SetDefault("commands.skipplaylist.messages.already_voted_error", "You have already voted to skip this playlist.")
+ viper.SetDefault("commands.skipplaylist.messages.voted", "%s has voted to skip the current playlist.")
viper.SetDefault("commands.toggleshuffle.aliases", []string{"toggleshuffle", "toggleshuf", "togshuf", "tsh"})
viper.SetDefault("commands.toggleshuffle.is_admin", true)
viper.SetDefault("commands.toggleshuffle.description", "Toggles automatic track shuffling on/off.")
+ viper.SetDefault("commands.toggleshuffle.messages.toggled_off", "Automatic shuffling has been toggled off.")
+ viper.SetDefault("commands.toggleshuffle.messages.toggled_on", "Automatic shuffling has been toggled on.")
viper.SetDefault("commands.version.aliases", []string{"version"})
viper.SetDefault("commands.version.is_admin", false)
viper.SetDefault("commands.version.description", "Outputs the current version of MumbleDJ.")
+ viper.SetDefault("commands.version.messages.version", "MumbleDJ version: %s")
viper.SetDefault("commands.volume.aliases", []string{"volume", "vol", "v"})
viper.SetDefault("commands.volume.is_admin", false)
viper.SetDefault("commands.volume.description", "Changes the volume if an argument is provided, outputs the current volume otherwise.")
+ viper.SetDefault("commands.volume.messages.parsing_error", "The requested volume could not be parsed.")
+ viper.SetDefault("commands.volume.messages.out_of_range_error", "Volumes must be between the values %.2f and %.2f.")
+ viper.SetDefault("commands.volume.messages.current_volume", "The current volume is %.2f.")
+ viper.SetDefault("commands.volume.messages.volume_changed", "%s has changed the volume to %.2f.")
}
// ReadConfigFile reads in the config file and updates the configuration accordingly.
diff --git a/commands/add.go b/commands/add.go
index b0bfda0..9466e3c 100644
--- a/commands/add.go
+++ b/commands/add.go
@@ -55,7 +55,7 @@ func (c *AddCommand) Execute(user *gumble.User, args ...string) (string, bool, e
)
if len(args) == 0 {
- return "", true, errors.New("A URL must be supplied with the add command")
+ return "", true, errors.New(viper.GetString("commands.add.messages.no_url_error"))
}
for _, arg := range args {
@@ -68,7 +68,7 @@ func (c *AddCommand) Execute(user *gumble.User, args ...string) (string, bool, e
}
if len(allTracks) == 0 {
- return "", true, errors.New("No valid tracks were found with the provided URL(s)")
+ return "", true, errors.New(viper.GetString("commands.add.messages.no_valid_tracks_error"))
}
numTooLong := 0
@@ -83,15 +83,15 @@ func (c *AddCommand) Execute(user *gumble.User, args ...string) (string, bool, e
}
if numAdded == 0 {
- return "", true, errors.New("Your track(s) were either too long or an error occurred while processing them. No track(s) have been added.")
+ return "", true, errors.New(viper.GetString("commands.add.messages.tracks_too_long_error"))
} else if numAdded == 1 {
- return fmt.Sprintf("%s added 1 track to the queue:
\"%s\" from %s",
+ return fmt.Sprintf(viper.GetString("commands.add.messages.one_track_added"),
user.Name, lastTrackAdded.GetTitle(), lastTrackAdded.GetService()), false, nil
}
- retString := fmt.Sprintf("%s added %d tracks to the queue.", user.Name, numAdded)
+ retString := fmt.Sprintf(viper.GetString("commands.add.messages.many_tracks_added"), user.Name, numAdded)
if numTooLong != 0 {
- retString += fmt.Sprintf("
%d tracks could not be added due to error or because they are too long.", numTooLong)
+ retString += fmt.Sprintf(viper.GetString("commands.add.messages.num_tracks_too_long"), numTooLong)
}
return retString, false, nil
}
diff --git a/commands/addnext.go b/commands/addnext.go
index 6449fce..ea9ed30 100644
--- a/commands/addnext.go
+++ b/commands/addnext.go
@@ -55,7 +55,7 @@ func (c *AddNextCommand) Execute(user *gumble.User, args ...string) (string, boo
)
if len(args) == 0 {
- return "", true, errors.New("A URL must be supplied with the addnext command")
+ return "", true, errors.New(viper.GetString("commands.add.messages.no_url_error"))
}
for _, arg := range args {
@@ -68,7 +68,7 @@ func (c *AddNextCommand) Execute(user *gumble.User, args ...string) (string, boo
}
if len(allTracks) == 0 {
- return "", true, errors.New("No valid tracks were found with the provided URL(s)")
+ return "", true, errors.New(viper.GetString("commands.add.messages.no_valid_tracks_error"))
}
numTooLong := 0
@@ -84,15 +84,15 @@ func (c *AddNextCommand) Execute(user *gumble.User, args ...string) (string, boo
}
if numAdded == 0 {
- return "", true, errors.New("Your track(s) were either too long or an error occurred while processing them. No track(s) have been added.")
+ return "", true, errors.New(viper.GetString("commands.add.messages.tracks_too_long_error"))
} else if numAdded == 1 {
- return fmt.Sprintf("%s added 1 track to the queue:
\"%s\" from %s",
+ return fmt.Sprintf(viper.GetString("commands.add.messages.one_track_added"),
user.Name, lastTrackAdded.GetTitle(), lastTrackAdded.GetService()), false, nil
}
- retString := fmt.Sprintf("%s added %d tracks to the queue.", user.Name, numAdded)
+ retString := fmt.Sprintf(viper.GetString("commands.add.messages.many_tracks_added"), user.Name, numAdded)
if numTooLong != 0 {
- retString += fmt.Sprintf("
%d tracks could not be added due to error or because they are too long.", numTooLong)
+ retString += fmt.Sprintf(viper.GetString("commands.add.messages.num_tracks_too_long"), numTooLong)
}
return retString, false, nil
}
diff --git a/commands/cachesize.go b/commands/cachesize.go
index a157255..27bfcad 100644
--- a/commands/cachesize.go
+++ b/commands/cachesize.go
@@ -47,9 +47,9 @@ func (c *CacheSizeCommand) Execute(user *gumble.User, args ...string) (string, b
const bytesInMiB = 1048576
if !viper.GetBool("cache.enabled") {
- return "", true, errors.New("Caching is currently disabled")
+ return "", true, errors.New(viper.GetString("commands.common_messages.caching_disabled_error"))
}
DJ.Cache.UpdateStatistics()
- return fmt.Sprintf("The current size of the cache is %.2v MiB.", DJ.Cache.TotalFileSize/bytesInMiB), true, nil
+ return fmt.Sprintf(viper.GetString("commands.cachesize.messages.current_size"), DJ.Cache.TotalFileSize/bytesInMiB), true, nil
}
diff --git a/commands/currenttrack.go b/commands/currenttrack.go
index 420fd95..6320804 100644
--- a/commands/currenttrack.go
+++ b/commands/currenttrack.go
@@ -52,9 +52,9 @@ func (c *CurrentTrackCommand) Execute(user *gumble.User, args ...string) (string
)
if currentTrack, err = DJ.Queue.CurrentTrack(); err != nil {
- return "", true, errors.New("There are no tracks in the queue")
+ return "", true, errors.New(viper.GetString("commands.common_messages.no_tracks_error"))
}
- return fmt.Sprintf("The current track is \"%s\", added by %s.",
+ return fmt.Sprintf(viper.GetString("commands.currenttrack.messages.current_track"),
currentTrack.GetTitle(), currentTrack.GetSubmitter()), true, nil
}
diff --git a/commands/forceskip.go b/commands/forceskip.go
index d718416..02754b0 100644
--- a/commands/forceskip.go
+++ b/commands/forceskip.go
@@ -45,11 +45,11 @@ func (c *ForceSkipCommand) IsAdminCommand() bool {
// return "This is a private message!", true, nil
func (c *ForceSkipCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
if DJ.Queue.Length() == 0 {
- return "", true, errors.New("The queue is currently empty. There are no tracks to skip")
+ return "", true, errors.New(viper.GetString("commands.common_messages.no_tracks_error"))
}
DJ.Queue.Skip()
- return fmt.Sprintf("The current track has been forcibly skipped by %s.",
+ return fmt.Sprintf(viper.GetString("commands.forceskip.messages.track_skipped"),
user.Name), false, nil
}
diff --git a/commands/forceskipplaylist.go b/commands/forceskipplaylist.go
index 83e6540..496ca48 100644
--- a/commands/forceskipplaylist.go
+++ b/commands/forceskipplaylist.go
@@ -52,15 +52,15 @@ func (c *ForceSkipPlaylistCommand) Execute(user *gumble.User, args ...string) (s
)
if currentTrack, err = DJ.Queue.CurrentTrack(); err != nil {
- return "", true, errors.New("The queue is currently empty. There are no playlists to skip")
+ return "", true, errors.New(viper.GetString("commands.common_messages.no_tracks_error"))
}
if playlist := currentTrack.GetPlaylist(); playlist == nil {
- return "", true, errors.New("The current track is not part of a playlist")
+ return "", true, errors.New(viper.GetString("commands.forceskipplaylist.messages.no_playlist_error"))
}
DJ.Queue.SkipPlaylist()
- return fmt.Sprintf("The current playlist has been forcibly skipped by %s.",
+ return fmt.Sprintf(viper.GetString("commands.forceskipplaylist.messages.playlist_skipped"),
user.Name), false, nil
}
diff --git a/commands/help.go b/commands/help.go
index df60e87..66f0e7e 100644
--- a/commands/help.go
+++ b/commands/help.go
@@ -58,7 +58,7 @@ func (c *HelpCommand) Execute(user *gumble.User, args ...string) (string, bool,
}
}
- totalString = "
Commands:
" + regularCommands
+ totalString = viper.GetString("commands.help.messages.commands_header") + regularCommands
isAdmin := false
if viper.GetBool("admins.enabled") {
@@ -68,7 +68,7 @@ func (c *HelpCommand) Execute(user *gumble.User, args ...string) (string, bool,
}
if isAdmin {
- totalString += "
Admin Commands:
" + adminCommands
+ totalString += viper.GetString("commands.help.messages.admin_commands_header") + adminCommands
}
return totalString, true, nil
diff --git a/commands/joinme.go b/commands/joinme.go
index 63045cc..a50f512 100644
--- a/commands/joinme.go
+++ b/commands/joinme.go
@@ -47,12 +47,12 @@ func (c *JoinMeCommand) IsAdminCommand() bool {
func (c *JoinMeCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
if DJ.AudioStream != nil && DJ.AudioStream.State() == gumbleffmpeg.StatePlaying &&
len(DJ.Client.Self.Channel.Users) > 1 {
- return "", true, errors.New("Users in another channel are listening to me.")
+ return "", true, errors.New(viper.GetString("commands.joinme.messages.others_are_listening_error"))
}
DJ.Client.Do(func() {
DJ.Client.Self.Move(user.Channel)
})
- return "I am now in your channel!", true, nil
+ return viper.GetString("commands.joinme.messages.in_your_channel"), true, nil
}
diff --git a/commands/listtracks.go b/commands/listtracks.go
index 93e7c58..c58865f 100644
--- a/commands/listtracks.go
+++ b/commands/listtracks.go
@@ -49,7 +49,7 @@ func (c *ListTracksCommand) IsAdminCommand() bool {
// return "This is a private message!", true, nil
func (c *ListTracksCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
if DJ.Queue.Length() == 0 {
- return "", true, errors.New("There are no tracks currently in the queue")
+ return "", true, errors.New(viper.GetString("commands.common_messages.no_tracks_error"))
}
numTracksToList := DJ.Queue.Length()
@@ -57,14 +57,14 @@ func (c *ListTracksCommand) Execute(user *gumble.User, args ...string) (string,
if parsedNum, err := strconv.Atoi(args[0]); err == nil {
numTracksToList = parsedNum
} else {
- return "", true, errors.New("An invalid integer was supplied")
+ return "", true, errors.New(viper.GetString("commands.listtracks.messages.invalid_integer_error"))
}
}
var buffer bytes.Buffer
DJ.Queue.Traverse(func(i int, track interfaces.Track) {
if i < numTracksToList {
- buffer.WriteString(fmt.Sprintf("%d: \"%s\", added by %s.
",
+ buffer.WriteString(fmt.Sprintf(viper.GetString("commands.listtracks.messages.track_listing"),
i+1, track.GetTitle(), track.GetSubmitter()))
}
})
diff --git a/commands/move.go b/commands/move.go
index b58d891..f3ec8fb 100644
--- a/commands/move.go
+++ b/commands/move.go
@@ -46,7 +46,7 @@ func (c *MoveCommand) IsAdminCommand() bool {
// return "This is a private message!", true, nil
func (c *MoveCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
if len(args) == 0 {
- return "", true, errors.New("A destination channel must be supplied to move the bot")
+ return "", true, errors.New(viper.GetString("commands.move.messages.no_channel_provided_error"))
}
channel := ""
for _, arg := range args {
@@ -58,8 +58,8 @@ func (c *MoveCommand) Execute(user *gumble.User, args ...string) (string, bool,
DJ.Client.Self.Move(DJ.Client.Channels.Find(channels...))
})
} else {
- return "", true, errors.New("The provided channel does not exist")
+ return "", true, errors.New(viper.GetString("commands.move.messages.channel_doesnt_exist_error"))
}
- return fmt.Sprintf("You have successfully moved the bot to %s.", channel), true, nil
+ return fmt.Sprintf(viper.GetString("commands.move.messages.move_successful"), channel), true, nil
}
diff --git a/commands/nexttrack.go b/commands/nexttrack.go
index 53fe51f..412d300 100644
--- a/commands/nexttrack.go
+++ b/commands/nexttrack.go
@@ -47,14 +47,14 @@ func (c *NextTrackCommand) IsAdminCommand() bool {
func (c *NextTrackCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
length := DJ.Queue.Length()
if length == 0 {
- return "", true, errors.New("There are no tracks in the queue")
+ return "", true, errors.New(viper.GetString("commands.common_messages.no_tracks_error"))
}
if length == 1 {
- return "", true, errors.New("The current track is the only track in the queue")
+ return "", true, errors.New(viper.GetString("commands.nexttrack.messages.current_track_only_error"))
}
nextTrack, _ := DJ.Queue.PeekNextTrack()
- return fmt.Sprintf("The next track is \"%s\", added by %s.",
+ return fmt.Sprintf(viper.GetString("commands.nexttrack.messages.next_track"),
nextTrack.GetTitle(), nextTrack.GetSubmitter()), true, nil
}
diff --git a/commands/numcached.go b/commands/numcached.go
index 0334f3e..4fb4232 100644
--- a/commands/numcached.go
+++ b/commands/numcached.go
@@ -46,10 +46,10 @@ func (c *NumCachedCommand) IsAdminCommand() bool {
// return "This is a private message!", true, nil
func (c *NumCachedCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
if !viper.GetBool("cache.enabled") {
- return "", true, errors.New("Caching is currently disabled")
+ return "", true, errors.New(viper.GetString("commands.common_messages.caching_disabled_error"))
}
DJ.Cache.UpdateStatistics()
- return fmt.Sprintf("There are currently %d items stored in the cache.",
+ return fmt.Sprintf(viper.GetString("commands.numcached.messages.num_cached"),
DJ.Cache.NumAudioFiles), true, nil
}
diff --git a/commands/numtracks.go b/commands/numtracks.go
index 9543477..28a9f96 100644
--- a/commands/numtracks.go
+++ b/commands/numtracks.go
@@ -46,8 +46,8 @@ func (c *NumTracksCommand) IsAdminCommand() bool {
func (c *NumTracksCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
length := DJ.Queue.Length()
if length == 1 {
- return "There is currently 1 track in the queue.", true, nil
+ return viper.GetString("commands.numtracks.messages.one_track"), true, nil
}
- return fmt.Sprintf("There are currently %d tracks in the queue.", length), true, nil
+ return fmt.Sprintf(viper.GetString("commands.numtracks.messages.plural_tracks"), length), true, nil
}
diff --git a/commands/pause.go b/commands/pause.go
index e55e7f0..b81918d 100644
--- a/commands/pause.go
+++ b/commands/pause.go
@@ -8,6 +8,7 @@
package commands
import (
+ "errors"
"fmt"
"github.com/layeh/gumble/gumble"
@@ -45,7 +46,7 @@ func (c *PauseCommand) IsAdminCommand() bool {
func (c *PauseCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
err := DJ.Queue.PauseCurrent()
if err != nil {
- return "", true, err
+ return "", true, errors.New(viper.GetString("commands.pause.messages.no_audio_error"))
}
- return fmt.Sprintf("%s has paused audio playback.", user.Name), false, nil
+ return fmt.Sprintf(viper.GetString("commands.pause.messages.paused"), user.Name), false, nil
}
diff --git a/commands/reload.go b/commands/reload.go
index 099f98b..75c9d54 100644
--- a/commands/reload.go
+++ b/commands/reload.go
@@ -47,6 +47,6 @@ func (c *ReloadCommand) Execute(user *gumble.User, args ...string) (string, bool
return "", true, err
}
- return "The configuration in the configuration file has been reloaded successfully.",
+ return viper.GetString("commands.reload.messages.reloaded"),
true, nil
}
diff --git a/commands/reset.go b/commands/reset.go
index 0267ae8..01892b1 100644
--- a/commands/reset.go
+++ b/commands/reset.go
@@ -45,7 +45,7 @@ func (c *ResetCommand) IsAdminCommand() bool {
// return "This is a private message!", true, nil
func (c *ResetCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
if DJ.Queue.Length() == 0 {
- return "", true, errors.New("The queue is already empty")
+ return "", true, errors.New(viper.GetString("commands.common_messages.no_tracks_error"))
}
if DJ.AudioStream != nil {
@@ -59,5 +59,5 @@ func (c *ResetCommand) Execute(user *gumble.User, args ...string) (string, bool,
return "", true, err
}
- return fmt.Sprintf("%s has reset the queue.", user.Name), false, nil
+ return fmt.Sprintf(viper.GetString("commands.reset.messages.queue_reset"), user.Name), false, nil
}
diff --git a/commands/resume.go b/commands/resume.go
index a7b7831..a83f995 100644
--- a/commands/resume.go
+++ b/commands/resume.go
@@ -8,6 +8,7 @@
package commands
import (
+ "errors"
"fmt"
"github.com/layeh/gumble/gumble"
@@ -45,7 +46,7 @@ func (c *ResumeCommand) IsAdminCommand() bool {
func (c *ResumeCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
err := DJ.Queue.ResumeCurrent()
if err != nil {
- return "", true, err
+ return "", true, errors.New(viper.GetString("commands.resume.messages.audio_error"))
}
- return fmt.Sprintf("%s has resumed audio playback.", user.Name), false, nil
+ return fmt.Sprintf(viper.GetString("commands.resume.messages.resumed"), user.Name), false, nil
}
diff --git a/commands/setcomment.go b/commands/setcomment.go
index 97ee7b5..d09291c 100644
--- a/commands/setcomment.go
+++ b/commands/setcomment.go
@@ -48,7 +48,7 @@ func (c *SetCommentCommand) Execute(user *gumble.User, args ...string) (string,
DJ.Client.Do(func() {
DJ.Client.Self.SetComment("")
})
- return "The comment for the bot has been successfully removed.", true, nil
+ return viper.GetString("commands.setcomment.messages.comment_removed"), true, nil
}
var newComment string
@@ -61,6 +61,6 @@ func (c *SetCommentCommand) Execute(user *gumble.User, args ...string) (string,
DJ.Client.Self.SetComment(newComment)
})
- return fmt.Sprintf("The comment for the bot has been successfully changed to the following: %s",
+ return fmt.Sprintf(viper.GetString("commands.setcomment.messages.comment_changed"),
newComment), true, nil
}
diff --git a/commands/shuffle.go b/commands/shuffle.go
index 87c055d..61f6a8f 100644
--- a/commands/shuffle.go
+++ b/commands/shuffle.go
@@ -45,13 +45,13 @@ func (c *ShuffleCommand) IsAdminCommand() bool {
func (c *ShuffleCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
length := DJ.Queue.Length()
if length == 0 {
- return "", true, errors.New("There are no tracks currently in the queue")
+ return "", true, errors.New(viper.GetString("commands.common_messages.no_tracks_error"))
}
if length <= 2 {
- return "", true, errors.New("There are not enough tracks in the queue to execute a shuffle")
+ return "", true, errors.New(viper.GetString("commands.shuffle.messages.not_enough_tracks_error"))
}
DJ.Queue.ShuffleTracks()
- return "The audio queue has been shuffled.", false, nil
+ return viper.GetString("commands.shuffle.messages.shuffled"), false, nil
}
diff --git a/commands/skip.go b/commands/skip.go
index 55f05ee..4de3153 100644
--- a/commands/skip.go
+++ b/commands/skip.go
@@ -45,11 +45,11 @@ func (c *SkipCommand) IsAdminCommand() bool {
// return "This is a private message!", true, nil
func (c *SkipCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
if DJ.Queue.Length() == 0 {
- return "", true, errors.New("The queue is currently empty. There is no track to skip")
+ return "", true, errors.New(viper.GetString("commands.common_messages.no_tracks_error"))
}
if err := DJ.Skips.AddTrackSkip(user); err != nil {
- return "", true, errors.New("You have already voted to skip this track")
+ return "", true, errors.New(viper.GetString("commands.skip.messages.already_voted_error"))
}
- return fmt.Sprintf("%s has voted to skip the current track.", user.Name), false, nil
+ return fmt.Sprintf(viper.GetString("commands.skip.messages.voted"), user.Name), false, nil
}
diff --git a/commands/skipplaylist.go b/commands/skipplaylist.go
index 00a9fdf..d119c9e 100644
--- a/commands/skipplaylist.go
+++ b/commands/skipplaylist.go
@@ -52,15 +52,15 @@ func (c *SkipPlaylistCommand) Execute(user *gumble.User, args ...string) (string
)
if currentTrack, err = DJ.Queue.CurrentTrack(); err != nil {
- return "", true, errors.New("The queue is currently empty. There is no playlist to skip")
+ return "", true, errors.New(viper.GetString("commands.common_messages.no_tracks_error"))
}
if playlist := currentTrack.GetPlaylist(); playlist == nil {
- return "", true, errors.New("The current track is not part of a playlist")
+ return "", true, errors.New(viper.GetString("commands.skipplaylist.messages.no_playlist_error"))
}
if err := DJ.Skips.AddPlaylistSkip(user); err != nil {
- return "", true, errors.New("You have already voted to skip this playlist")
+ return "", true, errors.New(viper.GetString("commands.skipplaylist.messages.already_voted_error"))
}
- return fmt.Sprintf("%s has voted to skip the current playlist.", user.Name), false, nil
+ return fmt.Sprintf(viper.GetString("commands.skipplaylist.messages.voted"), user.Name), false, nil
}
diff --git a/commands/toggleshuffle.go b/commands/toggleshuffle.go
index 19053b6..a6d3699 100644
--- a/commands/toggleshuffle.go
+++ b/commands/toggleshuffle.go
@@ -43,8 +43,8 @@ func (c *ToggleShuffleCommand) IsAdminCommand() bool {
func (c *ToggleShuffleCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
if viper.GetBool("queue.automatic_shuffle_on") {
viper.Set("queue.automatic_shuffle_on", false)
- return "Automatic shuffling has been toggled off.", false, nil
+ return viper.GetString("commands.toggleshuffle.messages.toggled_off"), false, nil
}
viper.Set("queue.automatic_shuffle_on", true)
- return "Automatic shuffling has been toggled on.", false, nil
+ return viper.GetString("commands.toggleshuffle.messages.toggled_on"), false, nil
}
diff --git a/commands/version.go b/commands/version.go
index 75c919e..0052b67 100644
--- a/commands/version.go
+++ b/commands/version.go
@@ -43,5 +43,5 @@ func (c *VersionCommand) IsAdminCommand() bool {
// Example return statement:
// return "This is a private message!", true, nil
func (c *VersionCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
- return fmt.Sprintf("MumbleDJ version: %s", DJ.Version), true, nil
+ return fmt.Sprintf(viper.GetString("commands.version.messages.version"), DJ.Version), true, nil
}
diff --git a/commands/volume.go b/commands/volume.go
index 465b537..ee0061e 100644
--- a/commands/volume.go
+++ b/commands/volume.go
@@ -47,16 +47,16 @@ func (c *VolumeCommand) IsAdminCommand() bool {
func (c *VolumeCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
if len(args) == 0 {
// Send the user the current volume level.
- return fmt.Sprintf("The current volume is %.2f.", DJ.Volume), true, nil
+ return fmt.Sprintf(viper.GetString("commands.volume.messages.current_volume"), DJ.Volume), true, nil
}
newVolume, err := strconv.ParseFloat(args[0], 32)
if err != nil {
- return "", true, errors.New("An error occurred while parsing the requested volume")
+ return "", true, errors.New(viper.GetString("commands.volume.messages.parsing_error"))
}
if newVolume < viper.GetFloat64("volume.lowest") || newVolume > viper.GetFloat64("volume.highest") {
- return "", true, fmt.Errorf("Volumes must be between the values %.2f and %.2f",
+ return "", true, fmt.Errorf(viper.GetString("commands.volume.messages.out_of_range_error"),
viper.GetFloat64("volume.lowest"), viper.GetFloat64("volume.highest"))
}
@@ -67,6 +67,6 @@ func (c *VolumeCommand) Execute(user *gumble.User, args ...string) (string, bool
}
DJ.Volume = newVolume32
- return fmt.Sprintf("%s has changed the volume to %.2f.",
+ return fmt.Sprintf(viper.GetString("commands.volume.messages.volume_changed"),
user.Name, newVolume32), false, nil
}
diff --git a/config.yaml b/config.yaml
index 9cd636f..49b39bc 100644
--- a/config.yaml
+++ b/config.yaml
@@ -140,17 +140,31 @@ commands:
# NOTE: Only one character (the first) is used.
prefix: "!"
+ common_messages:
+ no_tracks_error: "There are no tracks in the queue."
+ caching_disabled_error: "Caching is currently disabled."
+
# Below is a list of the commands supported by MumbleDJ. Each command has
# three configurable options:
# aliases: A list of names that can be used to execute the command.
# is_admin: true = only admins can execute the command, false = anyone can execute the command.
# description: Description shown for the command when the help command is executed.
+ # messages: Various messages that may be sent as a text message from the command. Useful for translating
+ # strings to other languages. Do NOT remove strings that begin with "%" (such as "%s", "%d", etc.)
+ # as they substituted with runtime data. Removing these strings will cause the bot to misbehave.
add:
aliases:
- "add"
- "a"
is_admin: false
description: "Adds a track or playlist from a media site to the queue."
+ messages:
+ no_url_error: "A URL must be supplied with the add command."
+ no_valid_tracks_error: "No valid tracks were found with the provided URL(s)."
+ tracks_too_long_error: "Your track(s) were either too long or an error occurred while processing them. No track(s) have been added."
+ one_track_added: "%s added 1 track to the queue:
%s from %s"
+ many_tracks_added: "%s added %d tracks to the queue."
+ num_tracks_too_long: "
%d tracks could not be added due to error or because they are too long."
addnext:
aliases:
@@ -158,6 +172,7 @@ commands:
- "an"
is_admin: true
description: "Adds a track or playlist from a media site as the next item in the queue."
+ # addnext uses the messages defined for add.
cachesize:
aliases:
@@ -165,6 +180,8 @@ commands:
- "cs"
is_admin: true
description: "Outputs the file size of the cache in MiB if caching is enabled."
+ messages:
+ current_size: "The current size of the cache is %.2v MiB."
currenttrack:
aliases:
@@ -173,6 +190,8 @@ commands:
- "current"
is_admin: false
description: "Outputs information about the current track in the queue if one exists."
+ messages:
+ current_track: "The current track is %s, added by %s."
forceskip:
aliases:
@@ -180,6 +199,8 @@ commands:
- "fs"
is_admin: true
description: "Immediately skips the current track."
+ messages:
+ track_skipped: "The current track has been forcibly skipped by %s."
forceskipplaylist:
aliases:
@@ -187,6 +208,9 @@ commands:
- "fsp"
is_admin: true
description: "Immediately skips the current playlist."
+ messages:
+ no_playlist_error: "The current track is not part of a playlist."
+ playlist_skipped: "The current playlist has been forcibly skipped by %s."
help:
aliases:
@@ -194,6 +218,9 @@ commands:
- "h"
is_admin: false
description: "Outputs this list of commands."
+ messages:
+ commands_header: "
Commands:
"
+ admin_commands_header: "
Admin Commands:
"
joinme:
aliases:
@@ -201,6 +228,9 @@ commands:
- "join"
is_admin: true
description: "Moves MumbleDJ into your current channel if not playing audio to someone else."
+ messages:
+ others_are_listening_error: "Users in another channel are listening to me."
+ in_your_channel: "I am now in your channel!"
kill:
aliases:
@@ -217,6 +247,9 @@ commands:
- "l"
is_admin: false
description: "Outputs a list of the tracks currently in the queue."
+ messages:
+ invalid_integer_error: "An invalid integer was supplied."
+ track_listing: "%d: %s, added by %s.
"
move:
aliases:
@@ -224,6 +257,10 @@ commands:
- "m"
is_admin: true
description: "Moves the bot into the Mumble channel provided via argument."
+ messages:
+ no_channel_provided_error: "A destination channel must be supplied to move the bot."
+ channel_doesnt_exist_error: "The provided channel does not exist."
+ move_successful: "You have successfully moved the bot to %s."
nexttrack:
aliases:
@@ -232,6 +269,9 @@ commands:
- "next"
is_admin: false
description: "Outputs information about the next track in the queue if one exists."
+ messages:
+ current_track_only_error: "The current track is the only track in the queue."
+ next_track: "The next track is %s, added by %s."
numcached:
aliases:
@@ -239,6 +279,8 @@ commands:
- "nc"
is_admin: true
description: "Outputs the number of tracks cached on disk if caching is enabled."
+ messages:
+ num_cached: "There are currently %d items stored in the cache."
numtracks:
aliases:
@@ -247,12 +289,18 @@ commands:
- "nt"
is_admin: false
description: "Outputs the number of tracks currently in the queue."
+ messages:
+ one_track: "There is currently 1 track in the queue."
+ plural_tracks: "There are currently %d tracks in the queue."
pause:
aliases:
- "pause"
is_admin: false
description: "Pauses audio playback."
+ messages:
+ no_audio_error: "Either the audio is already paused, or there are no tracks in the queue."
+ paused: "%s has paused audio playback."
reload:
aliases:
@@ -260,6 +308,8 @@ commands:
- "r"
is_admin: true
description: "Reloads the configuration file."
+ messages:
+ reloaded: "The configuration file has been successfully reloaded."
reset:
aliases:
@@ -267,12 +317,17 @@ commands:
- "re"
is_admin: true
description: "Resets the queue by removing all queue items."
+ messages:
+ queue_reset: "%s has reset the queue."
resume:
aliases:
- "resume"
is_admin: false
description: "Resumes audio playback."
+ messages:
+ audio_error: "Either the audio is already playing, or there are no tracks in the queue."
+ resumed: "%s has resumed audio playback."
setcomment:
aliases:
@@ -281,6 +336,9 @@ commands:
- "sc"
is_admin: true
description: "Sets the comment displayed next to MumbleDJ's username in Mumble."
+ messages:
+ comment_removed: "The comment for the bot has been successfully removed."
+ comment_changed: "The comment for the bot has been successfully changed to the following: %s"
shuffle:
aliases:
@@ -289,6 +347,9 @@ commands:
- "sh"
is_admin: true
description: "Randomizes the tracks currently in the queue."
+ messages:
+ not_enough_tracks_error: "There are not enough tracks in the queue to execute a shuffle."
+ shuffled: "The audio queue has been shuffled."
skip:
aliases:
@@ -296,6 +357,9 @@ commands:
- "s"
is_admin: false
description: "Places a vote to skip the current track."
+ messages:
+ already_voted_error: "You have already voted to skip this track."
+ voted: "%s has voted to skip the current track."
skipplaylist:
aliases:
@@ -303,6 +367,10 @@ commands:
- "sp"
is_admin: false
description: "Places a vote to skip the current playlist."
+ messages:
+ no_playlist_error: "The current track is not part of a playlist."
+ already_voted_error: "You have already voted to skip this playlist."
+ voted: "%s has voted to skip the current playlist."
toggleshuffle:
aliases:
@@ -312,6 +380,10 @@ commands:
- "tsh"
is_admin: true
description: "Toggles automatic track shuffling on/off."
+ messages:
+ toggled_off: "Automatic shuffling has been toggled off."
+ toggled_on: "Automatic shuffling has been toggled on."
+
version:
aliases:
@@ -319,10 +391,17 @@ commands:
- "v"
is_admin: false
description: "Outputs the current version of MumbleDJ."
+ messages:
+ version: "MumbleDJ version: %s"
volume:
aliases:
- "volume"
- "vol"
is_admin: false
- description: "Changes the volume if an argument is provided, outputs the current volume otherwise."
\ No newline at end of file
+ description: "Changes the volume if an argument is provided, outputs the current volume otherwise."
+ messages:
+ parsing_error: "The requested volume could not be parsed."
+ out_of_range_error: "Volumes must be between the values %.2f and %.2f."
+ current_volume: "The current volume is %.2f."
+ volume_changed: "%s has changed the volume to %.2f."
\ No newline at end of file
diff --git a/main.go b/main.go
index 60f121e..eb39731 100644
--- a/main.go
+++ b/main.go
@@ -32,7 +32,7 @@ func init() {
services.DJ = DJ
bot.DJ = DJ
- DJ.Version = "3.0.0"
+ DJ.Version = "3.0.1"
logrus.SetLevel(logrus.WarnLevel)
}