https://github.com/matthieugrieger/mumbledj/issues/27: Added song caching
This commit is contained in:
parent
d2056d57c0
commit
4957133c14
|
@ -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!
|
||||
|
|
2
Makefile
2
Makefile
|
@ -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
|
||||
|
|
|
@ -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
91
cache.go
Normal 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.")
|
||||
}
|
||||
}
|
34
commands.go
34
commands.go
|
@ -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() {
|
||||
|
|
7
main.go
7
main.go
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
34
song.go
|
@ -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
|
||||
|
|
11
strings.go
11
strings.go
|
@ -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>
|
||||
`
|
||||
|
||||
|
|
Reference in a new issue