Skip to content

Commit

Permalink
Big cleanup + use Spotify API
Browse files Browse the repository at this point in the history
  • Loading branch information
blixt committed Jun 29, 2012
1 parent 75e66d8 commit f8d4218
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 78 deletions.
153 changes: 85 additions & 68 deletions main.go
Original file line number Original file line Diff line number Diff line change
@@ -1,100 +1,117 @@
package main package main


import ( import (
"./echonest" "./spotify"
"./twealtime"
"./twitter" "./twitter"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
"strings" "strings"
) )


var stats = make(map[string]int) func getSpotifyUri(urlString string) (uri string) {

resp, err := http.Head(urlString)
func resolve(url string) { if err != nil || resp.StatusCode != 200 {
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 {
return return
} }


stats[p[2]]++ info, _ := url.Parse(resp.Request.URL.String())
//fmt.Printf("%#v\n", stats) if info.Host == "open.spotify.com" {

parts := strings.Split(info.Path, "/")
fmt.Println("URL:", url) if len(parts) != 3 || parts[1] != "track" {
if strings.HasPrefix(url, "http://www.last.fm") { return
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])
} }
uri = "spotify:track:" + parts[2]
} }

return
} }


func main() { 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`) 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 // Create a web socket server that will be serving the data to other apps.
stream := &twitter.StreamApi{"username", "password"} server := twealtime.NewServer()
// Create a channel that will be receiving tweets go server.Serve(":1337")
tweets := make(chan *twitter.Tweet)
// Start streaming tweets in a goroutine // Get the Twitter Stream API.
go stream.StatusesFilter([]string{"#nowplaying", "open.spotify.com", "spoti.fi"}, tweets) twitterStream := &twitter.StreamApi{"username", "password"}
// Fetch tweets as they come in // 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 { 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, "$") cleaned := cleanTweet.ReplaceAllString(tweet.Text, "$")
if matches := matchNowPlaying.FindStringSubmatch(cleaned); matches != nil {
results := make(chan *echonest.SongSearchQuery) // TODO: Make this nicer than an anonymous goroutine.
go en.SongSearch(matches[1], matches[2], results) go func(tweet *twitter.Tweet) {
go en.SongSearch(matches[2], matches[1], results) // First of all, try to find a Spotify URL directly in the tweet.

var uri string
artistResult := make(chan *echonest.ArtistExtractQuery) for _, url := range tweet.Entities.Urls {
go en.ArtistExtract(matches[1]+" "+matches[2], artistResult) uri = getSpotifyUri(url.ExpandedUrl)
qry := <-artistResult if uri != "" {
for _, artist := range qry.Response.Artists { break
fmt.Println("=>", artist.Name) }
} }


doOne := func(query *echonest.SongSearchQuery) { // Find artist and track name in the tweet.
fmt.Println("~~~~~") matches := matchNowPlaying.FindStringSubmatch(cleaned)
fmt.Printf("title: %#v, artist: %#v\n", query.Title, query.Artist) // Only do this if we didn't find a URI already.
if query.Error != nil { if uri == "" && matches != nil {
fmt.Println("FAILED:", query.Error.Error()) // 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 return
} }
for _, song := range query.Response.Songs {
fmt.Println("-", song.Title, "by", song.Artist_name) // Get the track of the result with the most results (which is most likely to be the correct one.)
uri := strings.Replace(song.Tracks[0].Foreign_id, "-WW", "", 1) if result1.Info.NumResults > result2.Info.NumResults {
fmt.Println(" =>", uri) uri = result1.Tracks[0].Href
} else if result2.Info.NumResults > result1.Info.NumResults {
uri = result2.Tracks[0].Href
} }
} }


doOne(<-results) // No URI was found; don't do anything.
doOne(<-results) if uri == "" {
} fmt.Println(":/", tweet.Text)
for _, url := range tweet.Entities.Urls { fmt.Println()
resolve(url.Expanded_url) return
} }
fmt.Println("----")
// 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)
} }
} }
116 changes: 116 additions & 0 deletions spotify/spotify.go
Original file line number Original file line Diff line number Diff line change
@@ -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
}
76 changes: 76 additions & 0 deletions twealtime/twealtime.go
Original file line number Original file line Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit f8d4218

Please sign in to comment.