-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
287 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |||
} | } | ||
} | } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |||
} |
Oops, something went wrong.