Permalink
Browse files

Big cleanup + use Spotify API

  • Loading branch information...
1 parent 75e66d8 commit f8d4218b98640f12d651103b663c3ba3073f4776 @blixt committed Jun 29, 2012
Showing with 287 additions and 78 deletions.
  1. +85 −68 main.go
  2. +116 −0 spotify/spotify.go
  3. +76 −0 twealtime/twealtime.go
  4. +10 −10 twitter/twitter.go
View
@@ -1,100 +1,117 @@
package main
import (
- "./echonest"
+ "./spotify"
+ "./twealtime"
"./twitter"
"fmt"
"net/http"
+ "net/url"
"regexp"
"strings"
)
-var stats = make(map[string]int)
-
-func resolve(url string) {
- resp, err := http.Head(url)
- if err != nil {
- fmt.Println("ERROR!!!", err.Error())
- return
- }
- if resp.StatusCode != 200 {
- return
- }
- url = resp.Request.URL.String()
-
- p := strings.Split(url, "/")
-
- // Skip links that cannot be to a specific resource
- if len(p) < 4 || len(p[3]) == 0 {
+func getSpotifyUri(urlString string) (uri string) {
+ resp, err := http.Head(urlString)
+ if err != nil || resp.StatusCode != 200 {
return
}
- stats[p[2]]++
- //fmt.Printf("%#v\n", stats)
-
- fmt.Println("URL:", url)
- if strings.HasPrefix(url, "http://www.last.fm") {
- pieces := strings.Split(url[19:], "/")
- switch len(pieces) {
- case 1:
- fmt.Println("Artist:", pieces[0])
- case 2:
- fmt.Println("Album:", pieces[1], "by", pieces[0])
- default:
- fmt.Println("Track:", pieces[2], "by", pieces[0])
+ info, _ := url.Parse(resp.Request.URL.String())
+ if info.Host == "open.spotify.com" {
+ parts := strings.Split(info.Path, "/")
+ if len(parts) != 3 || parts[1] != "track" {
+ return
}
+ uri = "spotify:track:" + parts[2]
}
+
+ return
}
func main() {
- matchNowPlaying := regexp.MustCompile(`(?:\A| )"?(\pL[\pL!'.-]*(?: \pL[\pL!'.-]*)*)"?\s*(?:'s|[♪–—~|:-]+|by)\s*"?(\pL[\pL!'.-]*(?: \pL[\pL!'.-]*)*)"?(?: |\z)`)
+ // Regexp that cleans up a tweet before scanning it for artist/track names.
cleanTweet := regexp.MustCompile(`\A(?i:now playing|listening to|escuchando a)|on (album|#)|del àlbum`)
+ // Regexp that finds artist/track names in a tweet.
+ matchNowPlaying := regexp.MustCompile(`(?:\A| )"?(\pL[\pL!'.-]*(?: \pL[\pL!'.()-]*)*)"?\s*(?:'s|[♪–—~|:/-]+|by)\s*"?(\pL[\pL!'.-]*(?: \pL[\pL!'.()-]*)*)"?(?:[ #]|\z)`)
- en := echonest.GetApi("<echo nest key>")
+ // Create a Spotify API object for searching the Spotify catalog.
+ sp := spotify.GetApi()
- // Get the Twitter Stream API
- stream := &twitter.StreamApi{"username", "password"}
- // Create a channel that will be receiving tweets
- tweets := make(chan *twitter.Tweet)
- // Start streaming tweets in a goroutine
- go stream.StatusesFilter([]string{"#nowplaying", "open.spotify.com", "spoti.fi"}, tweets)
- // Fetch tweets as they come in
+ // Create a web socket server that will be serving the data to other apps.
+ server := twealtime.NewServer()
+ go server.Serve(":1337")
+
+ // Get the Twitter Stream API.
+ twitterStream := &twitter.StreamApi{"username", "password"}
+ // Create a channel that will be receiving tweets.
+ tweets := make(chan *twitter.Tweet, 100)
+ // Start streaming tweets in a goroutine.
+ go twitterStream.StatusesFilter([]string{"#spotify", "#nowplaying", "open.spotify.com", "spoti.fi"}, tweets)
+ // Fetch tweets as they come in.
for tweet := range tweets {
- fmt.Printf("@%s (%d followers)\n", tweet.User.Screen_name, tweet.User.Followers_count)
- fmt.Println(tweet.Text)
cleaned := cleanTweet.ReplaceAllString(tweet.Text, "$")
- if matches := matchNowPlaying.FindStringSubmatch(cleaned); matches != nil {
- results := make(chan *echonest.SongSearchQuery)
- go en.SongSearch(matches[1], matches[2], results)
- go en.SongSearch(matches[2], matches[1], results)
-
- artistResult := make(chan *echonest.ArtistExtractQuery)
- go en.ArtistExtract(matches[1]+" "+matches[2], artistResult)
- qry := <-artistResult
- for _, artist := range qry.Response.Artists {
- fmt.Println("=>", artist.Name)
+
+ // TODO: Make this nicer than an anonymous goroutine.
+ go func(tweet *twitter.Tweet) {
+ // First of all, try to find a Spotify URL directly in the tweet.
+ var uri string
+ for _, url := range tweet.Entities.Urls {
+ uri = getSpotifyUri(url.ExpandedUrl)
+ if uri != "" {
+ break
+ }
}
- doOne := func(query *echonest.SongSearchQuery) {
- fmt.Println("~~~~~")
- fmt.Printf("title: %#v, artist: %#v\n", query.Title, query.Artist)
- if query.Error != nil {
- fmt.Println("FAILED:", query.Error.Error())
+ // Find artist and track name in the tweet.
+ matches := matchNowPlaying.FindStringSubmatch(cleaned)
+ // Only do this if we didn't find a URI already.
+ if uri == "" && matches != nil {
+ // Spotify search query format.
+ const format = `title:"%s" AND artist:"%s"`
+
+ // Create a channel for receiving results.
+ results := make(chan *spotify.SearchTrackQuery)
+ // Send off two simultaneous search requests to the Spotify search API, trying artist/track and the reverse
+ // (since we don't know if people wrote "Track - Artist" or "Artist - Track")
+ go sp.SearchTrack(fmt.Sprintf(format, matches[1], matches[2]), results)
+ go sp.SearchTrack(fmt.Sprintf(format, matches[2], matches[1]), results)
+ // Wait for the results to come in.
+ result1 := <-results
+ result2 := <-results
+
+ if result1.Error != nil || result2.Error != nil {
+ fmt.Println("!!", tweet.Text)
+ fmt.Println()
return
}
- for _, song := range query.Response.Songs {
- fmt.Println("-", song.Title, "by", song.Artist_name)
- uri := strings.Replace(song.Tracks[0].Foreign_id, "-WW", "", 1)
- fmt.Println(" =>", uri)
+
+ // Get the track of the result with the most results (which is most likely to be the correct one.)
+ if result1.Info.NumResults > result2.Info.NumResults {
+ uri = result1.Tracks[0].Href
+ } else if result2.Info.NumResults > result1.Info.NumResults {
+ uri = result2.Tracks[0].Href
}
}
- doOne(<-results)
- doOne(<-results)
- }
- for _, url := range tweet.Entities.Urls {
- resolve(url.Expanded_url)
- }
- fmt.Println("----")
+ // No URI was found; don't do anything.
+ if uri == "" {
+ fmt.Println(":/", tweet.Text)
+ fmt.Println()
+ return
+ }
+
+ // Send tweet info and track URI through the web socket server.
+ fmt.Println("<<", tweet.Text)
+ fmt.Println(">>", uri)
+ fmt.Println()
+
+ server.Send(twealtime.TrackMention{
+ Tweet: tweet.Text,
+ TwitterUser: tweet.User.ScreenName,
+ TwitterFollowers: tweet.User.FollowersCount,
+ TrackUri: uri,
+ })
+ }(tweet)
}
}
View
@@ -0,0 +1,116 @@
+package spotify
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+)
+
+type Album struct {
+ Name string
+ Href string
+ Released string
+ Availability interface{}
+}
+
+type Api struct {
+}
+
+type Artist struct {
+ Name string
+ Href string
+}
+
+type ExternalId struct {
+ Type string
+ Id string
+}
+
+type Info struct {
+ NumResults int `json:"num_results"`
+ Limit int
+ Offset int
+ Query string
+ Type string
+ Page int
+}
+
+type Track struct {
+ Name string
+ Href string
+ Artists []Artist
+ Album Album
+ TrackNumber string `json:"track-number"`
+ Length float32
+ Popularity string
+ ExternalIds []ExternalId `json:"external-ids"`
+}
+
+type query interface {
+ GetCallInfo() (path string, params map[string][]string)
+ setError(error)
+}
+
+type queryBase struct {
+ Error error
+}
+
+func (query *queryBase) setError(err error) {
+ query.Error = err
+}
+
+type SearchTrackQuery struct {
+ queryBase
+ Query string
+ Info Info
+ Tracks []Track
+}
+
+func (query *SearchTrackQuery) GetCallInfo() (path string, params map[string][]string) {
+ path = "/search/1/track.json"
+ params = map[string][]string{
+ "q": {query.Query}}
+ return
+}
+
+const (
+ BASE_URL = "http://ws.spotify.com"
+)
+
+func GetApi() *Api {
+ return &Api{}
+}
+
+func (api *Api) call(query query) {
+ path, params := query.GetCallInfo()
+
+ v := url.Values{}
+ for key, values := range params {
+ for _, value := range values {
+ v.Add(key, value)
+ }
+ }
+
+ resp, err := http.Get(fmt.Sprintf("%s%s?%s", BASE_URL, path, v.Encode()))
+ if err != nil {
+ query.setError(err)
+ return
+ }
+ defer resp.Body.Close()
+
+ if data, err := ioutil.ReadAll(resp.Body); err != nil {
+ query.setError(err)
+ } else {
+ json.Unmarshal(data, &query)
+ }
+}
+
+func (api *Api) SearchTrack(q string, out chan *SearchTrackQuery) {
+ query := &SearchTrackQuery{
+ Query: q}
+
+ api.call(query)
+ out <- query
+}
@@ -0,0 +1,76 @@
+package twealtime
+
+import (
+ "code.google.com/p/go.net/websocket"
+ "encoding/json"
+ "net/http"
+)
+
+type TrackMention struct {
+ Tweet string
+ TwitterUser string
+ TwitterFollowers int
+ TrackUri string
+}
+
+type Server struct {
+ sockets []*websocket.Conn
+}
+
+func NewServer() *Server {
+ return new(Server)
+}
+
+func (server *Server) Send(data interface{}) (err error) {
+ var bytes []byte
+
+ switch data.(type) {
+ case []byte:
+ bytes = data.([]byte)
+ case string:
+ bytes = []byte(data.(string))
+ default:
+ bytes, err = json.Marshal(data)
+ }
+
+ if err != nil {
+ return
+ }
+
+ // Write the data to all the sockets.
+ for _, socket := range server.sockets {
+ socket.Write(bytes)
+ }
+
+ return
+}
+
+func (server *Server) Serve(addr string) error {
+ http.Handle("/stream", websocket.Handler(func(ws *websocket.Conn) {
+ server.sockets = append(server.sockets, ws)
+
+ // Start reading from the socket.
+ for {
+ buffer := make([]byte, 4096)
+ if _, err := ws.Read(buffer); err != nil {
+ break
+ }
+ // TODO: Do something with received data.
+ }
+
+ // Remove socket.
+ for i, s := range server.sockets {
+ if s == ws {
+ server.sockets = append(server.sockets[:i], server.sockets[i+1:]...)
+ break
+ }
+ }
+ }))
+
+ err := http.ListenAndServe(addr, nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
Oops, something went wrong.

0 comments on commit f8d4218

Please sign in to comment.