Matthieu Grieger 2015-02-17 15:43:20 -08:00
parent d2056d57c0
commit 4957133c14
10 changed files with 214 additions and 12 deletions

View File

@ -1,6 +1,11 @@
MumbleDJ Changelog
==================
### February 17, 2015 -- `v2.6.0`
* Added caching system to MumbleDJ.
* Added configuration variables in `mumbledj.gcfg` for caching related settings (please note that caching is off by default).
* Added `!numcached` and `!cachesize` commands for admins.
### February 12, 2015 -- `v2.5.0`
* Updated dependencies and fixed code to match `gumble` API changes.
* Greatly simplified the song queue data structure. Some new bugs could potentially have arisen. Let me know if you find any!

View File

@ -1,6 +1,6 @@
all: mumbledj
mumbledj: main.go commands.go parseconfig.go strings.go song.go playlist.go songqueue.go
mumbledj: main.go commands.go parseconfig.go strings.go song.go playlist.go songqueue.go cache.go
go get github.com/nitrous-io/goop
rm -rf Goopfile.lock
goop install

View File

@ -25,6 +25,8 @@ Command | Description | Arguments | Admin | Example
**nextsong** | Outputs the title and name of the submitter of the next song in the queue if it exists. | None | No | `!nextsong`
**currentsong** | Outputs the title and name of the submitter of the song currently playing. | None | No | `!currentsong`
**setcomment** | Sets the comment for the bot. If no argument is given, the current comment will be removed. | None OR new_comment | Yes | `!setcomment Hello! I am a bot. Type !help for the available commands.`
**numcached** | Outputs the number of songs currently cached on disk. | None | Yes | `!numcached`
**cachesize** | Outputs the total file size of the cache in MB. | None | Yes | `!cachesize`
**kill** | Safely cleans the bot environment and disconnects from the server. Please use this command to stop the bot instead of force closing, as the kill command deletes any remaining songs in the `~/.mumbledj/songs` directory. | None | Yes | `!kill`

91
cache.go Normal file
View File

@ -0,0 +1,91 @@
/*
* MumbleDJ
* By Matthieu Grieger
* cache.go
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
*/
package main
import (
"errors"
"fmt"
"io/ioutil"
"os"
"sort"
"time"
)
type ByAge []os.FileInfo
func (a ByAge) Len() int {
return len(a)
}
func (a ByAge) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByAge) Less(i, j int) bool {
return time.Since(a[i].ModTime()) < time.Since(a[j].ModTime())
}
type SongCache struct {
NumSongs int
TotalFileSize int64
}
func NewSongCache() *SongCache {
newCache := &SongCache{
NumSongs: 0,
TotalFileSize: 0,
}
return newCache
}
func (c *SongCache) GetNumSongs() int {
songs, _ := ioutil.ReadDir(fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir))
return len(songs)
}
func (c *SongCache) GetCurrentTotalFileSize() int64 {
var totalSize int64 = 0
songs, _ := ioutil.ReadDir(fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir))
for _, song := range songs {
totalSize += song.Size()
}
return totalSize
}
func (c *SongCache) CheckMaximumDirectorySize() {
for c.GetCurrentTotalFileSize() > (dj.conf.Cache.MaximumSize * 1048576) {
if err := c.ClearOldest(); err != nil {
break
}
}
}
func (c *SongCache) Update() {
c.NumSongs = c.GetNumSongs()
c.TotalFileSize = c.GetCurrentTotalFileSize()
}
func (c *SongCache) ClearExpired() {
for _ = range time.Tick(5 * time.Minute) {
songs, _ := ioutil.ReadDir(fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir))
for _, song := range songs {
hours := time.Since(song.ModTime()).Hours()
if hours >= dj.conf.Cache.ExpireTime && (dj.queue.CurrentSong().youtubeId+".m4a") != song.Name() {
os.Remove(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, song.Name()))
}
}
}
}
func (c *SongCache) ClearOldest() error {
songs, _ := ioutil.ReadDir(fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir))
sort.Sort(ByAge(songs))
if (dj.queue.CurrentSong().youtubeId + ".m4a") != songs[0].Name() {
return os.Remove(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, songs[0].Name()))
} else {
return errors.New("Song is currently playing.")
}
}

View File

@ -130,6 +130,20 @@ func parseCommand(user *gumble.User, username, command string) {
} else {
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
}
// Numcached command
case dj.conf.Aliases.NumCachedAlias:
if dj.HasPermission(username, dj.conf.Permissions.AdminNumCached) {
numCached(user)
} else {
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
}
// Cachesize command
case dj.conf.Aliases.CacheSizeAlias:
if dj.HasPermission(username, dj.conf.Permissions.AdminCacheSize) {
cacheSize(user)
} else {
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
}
// Kill command
case dj.conf.Aliases.KillAlias:
if dj.HasPermission(username, dj.conf.Permissions.AdminKill) {
@ -378,6 +392,26 @@ func setComment(user *gumble.User, comment string) {
dj.SendPrivateMessage(user, COMMENT_UPDATED_MSG)
}
// Performs numcached functionality. Displays the number of songs currently cached on disk at ~/.mumbledj/songs.
func numCached(user *gumble.User) {
if dj.conf.Cache.Enabled {
dj.cache.Update()
dj.SendPrivateMessage(user, fmt.Sprintf(NUM_CACHED_MSG, dj.cache.NumSongs))
} else {
dj.SendPrivateMessage(user, CACHE_NOT_ENABLED_MSG)
}
}
// Performs cachesize functionality. Displays the total file size of the cached audio files.
func cacheSize(user *gumble.User) {
if dj.conf.Cache.Enabled {
dj.cache.Update()
dj.SendPrivateMessage(user, fmt.Sprintf(CACHE_SIZE_MSG, float64(dj.cache.TotalFileSize/1048576)))
} else {
dj.SendPrivateMessage(user, CACHE_NOT_ENABLED_MSG)
}
}
// Performs kill functionality. First cleans the ~/.mumbledj/songs directory to get rid of any
// excess m4a files. The bot then safely disconnects from the server.
func kill() {

View File

@ -30,6 +30,7 @@ type mumbledj struct {
audioStream *gumble_ffmpeg.Stream
homeDir string
playlistSkips map[string][]string
cache *SongCache
}
// OnConnect event. First moves MumbleDJ into the default channel specified
@ -62,6 +63,11 @@ func (dj *mumbledj) OnConnect(e *gumble.ConnectEvent) {
dj.client.AudioEncoder.SetApplication(gopus.Audio)
dj.client.Self.SetComment(dj.conf.General.DefaultComment)
if dj.conf.Cache.Enabled {
dj.cache.Update()
go dj.cache.ClearExpired()
}
}
// OnDisconnect event. Terminates MumbleDJ thread.
@ -121,6 +127,7 @@ var dj = mumbledj{
keepAlive: make(chan bool),
queue: NewSongQueue(),
playlistSkips: make(map[string][]string),
cache: NewSongCache(),
}
// Main function, but only really performs startup tasks. Grabs and parses commandline

View File

@ -23,6 +23,21 @@ PlaylistSkipRatio = 0.5
DefaultComment = "Hello! I am a bot. Type !help for a list of commands."
[Cache]
# Cache songs as they are downloaded?
# DEFAULT VALUE: false
Enabled = false
# Maximum total file size of cache directory (in MB)
# DEFAULT VALUE: 512
MaximumSize = 512
# Period of time that should elapse before a song is cleared from the cache (in hours)
# DEFAULT VALUE: 24
ExpireTime = 24
[Volume]
# Default volume
@ -92,6 +107,14 @@ CurrentSongAlias = "currentsong"
# DEFAULT VALUE: "setcomment"
SetCommentAlias = "setcomment"
# Alias used for numcached command
# DEFAULT VALUE: "numcached"
NumCachedAlias = "numcached"
# Alias used for cachesize command
# DEFAULT VALUE: "cachesize"
CacheSizeAlias = "cachesize"
# Alias used for kill command
# DEFAULT VALUE: "kill"
KillAlias = "kill"
@ -160,6 +183,14 @@ AdminCurrentSong = false
# DEFAULT VALUE: true
AdminSetComment = true
# Make numcached an admin command?
# DEFAULT VALUE: true
AdminNumCached = true
# Make cachesize an admin command?
# DEFAULT VALUE: true
AdminCacheSize = true
# Make kill an admin command?
# DEFAULT VALUE: true (I recommend never changing this to false)
AdminKill = true

View File

@ -21,6 +21,11 @@ type DjConfig struct {
PlaylistSkipRatio float32
DefaultComment string
}
Cache struct {
Enabled bool
MaximumSize int64
ExpireTime float64
}
Volume struct {
DefaultVolume float32
LowestVolume float32
@ -41,6 +46,8 @@ type DjConfig struct {
NextSongAlias string
CurrentSongAlias string
SetCommentAlias string
NumCachedAlias string
CacheSizeAlias string
KillAlias string
}
Permissions struct {
@ -58,6 +65,8 @@ type DjConfig struct {
AdminNextSong bool
AdminCurrentSong bool
AdminSetComment bool
AdminNumCached bool
AdminCacheSize bool
AdminKill bool
}
}

34
song.go
View File

@ -72,13 +72,21 @@ func NewSong(user, id string, playlist *Playlist) (*Song, error) {
return song, nil
}
// Downloads the song via youtube-dl. All downloaded songs are stored in ~/.mumbledj/songs and should be automatically cleaned.
// Downloads the song via youtube-dl if it does not already exist on disk.
// All downloaded songs are stored in ~/.mumbledj/songs and should be automatically cleaned.
func (s *Song) Download() error {
cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s.m4a`, s.youtubeId), "--format", "m4a", s.youtubeId)
if err := cmd.Run(); err == nil {
return nil
if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.youtubeId)); os.IsNotExist(err) {
cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s.m4a`, s.youtubeId), "--format", "m4a", s.youtubeId)
if err := cmd.Run(); err == nil {
if dj.conf.Cache.Enabled {
dj.cache.CheckMaximumDirectorySize()
}
return nil
} else {
return errors.New("Song download failed.")
}
} else {
return errors.New("Song download failed.")
return nil
}
}
@ -98,14 +106,18 @@ func (s *Song) Play() {
}
}
// Deletes the song from ~/.mumbledj/songs.
// Deletes the song from ~/.mumbledj/songs if the cache is disabled.
func (s *Song) Delete() error {
filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.youtubeId)
if _, err := os.Stat(filePath); err == nil {
if err := os.Remove(filePath); err == nil {
return nil
if dj.conf.Cache.Enabled == false {
filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.youtubeId)
if _, err := os.Stat(filePath); err == nil {
if err := os.Remove(filePath); err == nil {
return nil
} else {
return errors.New("Error occurred while deleting audio file.")
}
} else {
return errors.New("Error occurred while deleting audio file.")
return nil
}
} else {
return nil

View File

@ -56,6 +56,15 @@ const INVALID_YOUTUBE_ID_MSG = "The YouTube URL you supplied did not contain a v
// Message shown to user when they successfully update the bot's comment.
const COMMENT_UPDATED_MSG = "The comment for the bot has successfully been updated."
// Message shown to user when they request to see the number of songs cached on disk.
const NUM_CACHED_MSG = "There are currently %d songs cached on disk."
// Message shown to user when they request to see the total size of the cache.
const CACHE_SIZE_MSG = "The cache is currently %g MB in size."
// Message shown to user when they attempt to issue a cache-related command when caching is not enabled.
const CACHE_NOT_ENABLED_MSG = "The cache is not currently enabled."
// Message shown to a channel when a new song starts playing.
const NOW_PLAYING_HTML = `
<table>
@ -128,6 +137,8 @@ const HELP_HTML = `<br/>
<p><b>!move </b>- Moves MumbleDJ into channel if it exists.</p>
<p><b>!reload</b> - Reloads mumbledj.gcfg configuration settings.</p>
<p><b>!setcomment</b> - Sets the comment for the bot.</p>
<p><b>!numcached</b></p> - Outputs the number of songs cached on disk.</p>
<p><b>!cachesize</b></p> - Outputs the total file size of the cache in MB.</p>
<p><b>!kill</b> - Safely cleans the bot environment and disconnects from the server.</p>
`