diff --git a/config.gcfg b/config.gcfg index 5c7a595..02393e6 100644 --- a/config.gcfg +++ b/config.gcfg @@ -55,6 +55,24 @@ LowestVolume = 0.01 # DEFAULT VALUE: 0.8 HighestVolume = 0.8 + +[Web] + +# Enable web browser control +# DEFAULT VALUE: true +WebBrowser: true + +# Port for web browser, 80 provides links with no port +# DEFAULT VALUE: 9563 +WebPort: 9563 + +# Web address to provide links for +# DEFAULT VALUE: http://{{IP}}:{{PORT}}/ +WebAddress: http://{{IP}}:{{PORT}}/ + +# Can users have their own web page +# DEFAULT VALUE: false +CustomWebPage: false [Aliases] diff --git a/index.html b/index.html index 9ef2b91..0922733 100644 --- a/index.html +++ b/index.html @@ -1,38 +1,49 @@ - - - {{.User}} - mumbledj - - + - - -

Add Song Form

- - - - - - + function txtBox(type) { + var txt = $("#textbox"); + api(type, txt.attr("value")); + txt.attr("value", ""); + } + + + +

Add Song Form

+ + + + + +
+ + \ No newline at end of file diff --git a/main.go b/main.go index ba85c0c..1dd4907 100644 --- a/main.go +++ b/main.go @@ -142,12 +142,14 @@ func PerformStartupChecks() { } } +// Prints out messages only if verbose flag is true func Verbose(msg string) { if dj.verbose { fmt.Printf(msg + "\n") } } +// Checks to see if an object is nil func isNil(a interface{}) bool { defer func() { recover() }() return a == nil || reflect.ValueOf(a).IsNil() diff --git a/service_soundcloud.go b/service_soundcloud.go new file mode 100644 index 0000000..34944e7 --- /dev/null +++ b/service_soundcloud.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "regexp" + "strconv" + "strings" + "time" + + "github.com/jmoiron/jsonq" + "github.com/layeh/gumble/gumble" + "github.com/layeh/gumble/gumble_ffmpeg" +) + +// Regular expressions for soundcloud urls +var soundcloudSongPattern = `https?:\/\/(www)?\.soundcloud\.com\/([\w-]+)\/([\w-]+)` +var soundcloudPlaylistPattern = `https?:\/\/(www)?\.soundcloud\.com\/([\w-]+)\/sets\/([\w-]+)` + +// ------ +// TYPES +// ------ + +// YouTube implements the Service interface +type SoundCloud struct{} + +// YouTubeSong holds the metadata for a song extracted from a YouTube video. +type SoundCloudSong struct { + submitter string + title string + id string + offset int + filename string + duration string + thumbnail string + skippers []string + playlist Playlist + dontSkip bool +} + +// YouTubePlaylist holds the metadata for a YouTube playlist. +type SoundCloudPlaylist struct { + id string + title string +} + +// ------------------ +// SOUNDCLOUD SERVICE +// ------------------ + +// Name of the service +func (sc SoundCloud) ServiceName() string { + return "SoundCloud" +} + +// Checks to see if service will accept URL +func (sc SoundCloud) URLRegex(url string) bool { + return RegexpFromURL(url, []string{soundcloudSongPattern, soundcloudPlaylistPattern}) != nil +} + +// Creates the requested song/playlist and adds to the queue +func (sc SoundCloud) NewRequest(user *gumble.User, url string) (string, error) { + var apiResponse *jsonq.JsonQuery + var err error + url := fmt.Sprintf("http://api.soundcloud.com/resolve?url=%s&client_id=%s", url, os.Getenv("SOUNDCLOUD_API_KEY")) + if apiResponse, err = PerformGetRequest(url); err != nil { + return nil, errors.New(INVALID_API_KEY) + } + + title, _ := apiResponse.String("title") + tracks, err := apiResponse.ArrayOfObjects("tracks") + + if err == nil { + if re.MatchString(url) { + // PLAYLIST + if dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) { + playlist, err := sc.NewPlaylist(user.Name, url) + return playlist.Title(), err + } else { + return "", errors.New("NO_PLAYLIST_PERMISSION") + } + } else { + + // SONG + song, err := sc.NewSong(user.Name, url, nil) + return song.Title(), err + } + } else { + return "", err + } +} diff --git a/service_youtube.go b/service_youtube.go index 12b925c..5da5607 100644 --- a/service_youtube.go +++ b/service_youtube.go @@ -110,7 +110,7 @@ func (yt YouTube) NewSong(user, id, offset string, playlist *YouTubePlaylist) (* var err error url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s", id, os.Getenv("YOUTUBE_API_KEY")) - if apiResponse, err = yt.PerformGetRequest(url); err != nil { + if apiResponse, err = PerformGetRequest(url); err != nil { return nil, errors.New(INVALID_API_KEY) } @@ -206,7 +206,7 @@ func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) { // Retrieve title of playlist url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlists?part=snippet&id=%s&key=%s", id, os.Getenv("YOUTUBE_API_KEY")) - if apiResponse, err = yt.PerformGetRequest(url); err != nil { + if apiResponse, err = PerformGetRequest(url); err != nil { return nil, err } title, _ := apiResponse.String("items", "0", "snippet", "title") @@ -219,7 +219,7 @@ func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) { // Retrieve items in playlist url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=%s&key=%s", id, os.Getenv("YOUTUBE_API_KEY")) - if apiResponse, err = yt.PerformGetRequest(url); err != nil { + if apiResponse, err = PerformGetRequest(url); err != nil { return nil, err } numVideos, _ := apiResponse.Int("pageInfo", "totalResults") @@ -469,7 +469,7 @@ func (p *YouTubePlaylist) Title() string { // ----------- // PerformGetRequest does all the grunt work for a YouTube HTTPS GET request. -func (yt YouTube) PerformGetRequest(url string) (*jsonq.JsonQuery, error) { +func PerformGetRequest(url string) (*jsonq.JsonQuery, error) { jsonString := "" if response, err := http.Get(url); err == nil { @@ -482,7 +482,7 @@ func (yt YouTube) PerformGetRequest(url string) (*jsonq.JsonQuery, error) { if response.StatusCode == 403 { return nil, errors.New("Invalid API key supplied.") } - return nil, errors.New("Invalid YouTube ID supplied.") + return nil, errors.New("Invalid ID supplied.") } } else { return nil, errors.New("An error occurred while receiving HTTP GET response.") diff --git a/songqueue.go b/songqueue.go index f1bdc1c..54e3610 100644 --- a/songqueue.go +++ b/songqueue.go @@ -77,6 +77,14 @@ func (q *SongQueue) Traverse(visit func(i int, s Song)) { } } +// Gets the song at a specific point in the queue +func (q *SongQueue) Get(int i) (Song, error) { + if q.Len() > i+1 { + return q.queue[i], nil + } + return nil, errors.New("Out of Bounds") +} + // OnSongFinished event. Deletes Song that just finished playing, then queues the next Song (if exists). func (q *SongQueue) OnSongFinished() { resetOffset, _ := time.ParseDuration(fmt.Sprintf("%ds", 0)) diff --git a/web.go b/web.go index 4cd57e4..d9873d7 100644 --- a/web.go +++ b/web.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "html" "html/template" @@ -28,6 +29,21 @@ type Page struct { User string } +type status struct { + Error bool + ErrorMsg string + Queue []SongInfo +} +type SongInfo struct { + TitleID string + PlaylistID string + Title string + Playlist string + Submitter string + Duration string + Thumbnail string +} + var external_ip = "" func NewWebServer(port int) *WebServer { @@ -41,9 +57,10 @@ func NewWebServer(port int) *WebServer { func (web *WebServer) makeWeb() { http.HandleFunc("/", web.homepage) - http.HandleFunc("/add", web.add) - http.HandleFunc("/volume", web.volume) - http.HandleFunc("/skip", web.skip) + http.HandleFunc("/api/add", web.add) + http.HandleFunc("/api/volume", web.volume) + http.HandleFunc("/api/skip", web.skip) + http.HandleFunc("/api/status", web.status) http.ListenAndServe(":"+strconv.Itoa(web.port), nil) } @@ -52,8 +69,14 @@ func (web *WebServer) homepage(w http.ResponseWriter, r *http.Request) { if uname == nil { fmt.Fprintf(w, "Invalid Token") } else { - cwd, _ := os.Getwd() - t, err := template.ParseFiles(filepath.Join(cwd, "./.mumbledj/web/index.html")) + var webpage = uname.Name + + // Check to see if user has a custom webpage + if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/web/%s.html", dj.homeDir, uname.Name)); os.IsNotExist(err) { + webpage = "index" + } + + t, err := template.ParseFiles(fmt.Sprintf("%s/.mumbledj/songs/%s.html", dj.homeDir, uname.Name)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -103,6 +126,33 @@ func (web *WebServer) skip(w http.ResponseWriter, r *http.Request) { } } +func (web *WebServer) status(w http.ResponseWriter, r *http.Request) { + var uname = web.token_client[r.FormValue("token")] + if uname == nil { + fmt.Fprintf(w, string(json.MarshalIndent(&Status{true, "Invalid Token"}))) + } else { + // Generate song queue + var songsInQueue [dj.queue.Len()]SongInfo + for i := 0; i < dj.queue.Len(); i++ { + songItem := dj.queue.Get(i) + songs[i] = &SongInfo{ + TitleID: songItem.ID(), + Title: songItem.Title(), + Submitter: songItem.Submitter(), + Duration: songItem.Duration(), + Thumbnail: songItem.Thumbnail(), + } + if !isNil(songItem.Playlist()) { + songs[i].PlaylistID = songItem.Playlist().ID() + songs[i].Playlist = songItem.Playlist().Title() + } + } + + // Output status + fmt.Fprintf(w, string(json.MarshalIndent(&Status{false, "", songsInQueue}))) + } +} + func (website *WebServer) GetWebAddress(user *gumble.User) { Verbose("Port number: " + strconv.Itoa(web.port)) if web.client_token[user] != "" { @@ -110,7 +160,7 @@ func (website *WebServer) GetWebAddress(user *gumble.User) { } // dealing with collisions var firstLoop = true - for firstLoop || web.token_client[web.client_token[user]] != nil { + for firstLoop || web.token_client[web.client_token[user]] != nil || web.client_token[user] == "api" { web.client_token[user] = randSeq(10) firstLoop = false }