Skip to content

Commit d5367a8

Browse files
Merge pull request #3 from kylegrantlucas/spotify
2 parents a259856 + 9d9e8ee commit d5367a8

File tree

233 files changed

+56030
-813
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

233 files changed

+56030
-813
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ A small tool for my command line, gets the currently playing spotify track and t
66

77
`$ go install github.com/kylegrantlucas/lyricpiece@latest`
88

9+
## Setup
10+
11+
Using this cli tool requires setting up a Spotify OAuth2 Application so we can call the API for the user's current info.
12+
13+
1. Register an application at: https://developer.spotify.com/my-applications/
14+
- Use `http://localhost:8080/callback` as the redirect URI
15+
16+
2. Set the `SPOTIFY_ID` environment variable to the client ID you got in step 1.
17+
3. Set the `SPOTIFY_SECRET` environment variable to the client secret from step 1.
18+
919
## Usage
1020

1121
`$ lyricpiece`

go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@ module github.com/kylegrantlucas/lyricpiece
33
go 1.19
44

55
require (
6-
github.com/andybrewer/mack v0.0.0-20220307193339-22e922cc18af
76
github.com/boltdb/bolt v1.3.1
87
github.com/rhnvrm/lyric-api-go v0.1.4
8+
github.com/zmb3/spotify/v2 v2.3.0
9+
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5
910
)
1011

1112
require (
1213
github.com/PuerkitoBio/goquery v1.8.0 // indirect
1314
github.com/andybalholm/cascadia v1.3.1 // indirect
15+
github.com/golang/protobuf v1.5.2 // indirect
16+
github.com/google/go-cmp v0.5.6 // indirect
1417
github.com/gopherjs/gopherjs v1.17.2 // indirect
1518
github.com/gosimple/slug v1.13.0 // indirect
1619
github.com/gosimple/unidecode v1.0.1 // indirect
1720
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b // indirect
1821
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
22+
google.golang.org/appengine v1.6.7 // indirect
23+
google.golang.org/protobuf v1.27.1 // indirect
1924
)

go.sum

Lines changed: 377 additions & 2 deletions
Large diffs are not rendered by default.

main.go

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ package main
33
import (
44
"fmt"
55
"os"
6-
"sync"
76

8-
"github.com/andybrewer/mack"
97
"github.com/kylegrantlucas/lyricpiece/lyricpiece"
8+
"github.com/kylegrantlucas/lyricpiece/spotify"
109
)
1110

1211
func main() {
@@ -15,19 +14,37 @@ func main() {
1514
fmt.Fprintln(os.Stderr, err)
1615
}
1716

18-
client, err := lyricpiece.NewClient(dbPath)
17+
// call to spotify to get the currently playing song
18+
spotClient, err := spotify.NewClient(dbPath)
19+
if err != nil {
20+
fmt.Fprintln(os.Stderr, err)
21+
}
22+
23+
song, err := spotClient.GetCurrentlyPlaying()
24+
if err != nil {
25+
fmt.Fprintln(os.Stderr, err)
26+
}
27+
28+
// close up the DB so the lyricpiece client can use it
29+
err = spotClient.Close()
1930
if err != nil {
2031
fmt.Fprintln(os.Stderr, err)
2132
}
22-
defer client.Close()
2333

24-
song := getCurrentSong()
25-
lyricPiece, err := client.GetLyricPiece(song)
34+
client, err := lyricpiece.NewClient(dbPath)
2635
if err != nil {
2736
fmt.Fprintln(os.Stderr, err)
2837
}
38+
defer client.Close()
39+
40+
if song != nil {
41+
lyricPiece, err := client.GetLyricPiece(*song)
42+
if err != nil {
43+
fmt.Fprintln(os.Stderr, err)
44+
}
2945

30-
fmt.Print(lyricPiece)
46+
fmt.Print(lyricPiece)
47+
}
3148
}
3249

3350
func buildDBPath() (string, error) {
@@ -44,23 +61,3 @@ func buildDBPath() (string, error) {
4461

4562
return path + "/lyricpiece.db", nil
4663
}
47-
48-
func getCurrentSong() lyricpiece.Song {
49-
var track, artist string
50-
wg := &sync.WaitGroup{}
51-
wg.Add(2)
52-
53-
go func() {
54-
track, _ = mack.Tell("Spotify", "name of current track as string")
55-
wg.Done()
56-
}()
57-
58-
go func() {
59-
artist, _ = mack.Tell("Spotify", "artist of current track as string")
60-
wg.Done()
61-
}()
62-
63-
wg.Wait()
64-
65-
return lyricpiece.Song{Title: track, Artist: artist}
66-
}

spotify/client.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// 1. Register an application at: https://developer.spotify.com/my-applications/
2+
// - Use "http://localhost:8080/callback" as the redirect URI
3+
4+
// 2. Set the SPOTIFY_ID environment variable to the client ID you got in step 1.
5+
// 3. Set the SPOTIFY_SECRET environment variable to the client secret from step 1.
6+
package spotify
7+
8+
import (
9+
"context"
10+
"encoding/json"
11+
"fmt"
12+
"log"
13+
"net/http"
14+
15+
"github.com/boltdb/bolt"
16+
"github.com/kylegrantlucas/lyricpiece/lyricpiece"
17+
spot "github.com/zmb3/spotify/v2"
18+
spotifyauth "github.com/zmb3/spotify/v2/auth"
19+
"golang.org/x/oauth2"
20+
)
21+
22+
const redirectURI = "http://localhost:8080/callback"
23+
24+
var (
25+
auth = spotifyauth.New(spotifyauth.WithRedirectURL(redirectURI), spotifyauth.WithScopes(spotifyauth.ScopeUserReadCurrentlyPlaying, spotifyauth.ScopeUserReadPlaybackState))
26+
ch = make(chan *oauth2.Token)
27+
state = "abc123"
28+
)
29+
30+
type Client struct {
31+
db *bolt.DB
32+
client *spot.Client
33+
}
34+
35+
func NewClient(dbPath string) (*Client, error) {
36+
db, err := bolt.Open(dbPath, 0600, nil)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
client := &Client{
42+
db: db,
43+
}
44+
45+
client.auth()
46+
47+
return client, nil
48+
}
49+
50+
func (c *Client) Close() error {
51+
return c.db.Close()
52+
}
53+
54+
// get currently playing song
55+
func (c *Client) GetCurrentlyPlaying() (*lyricpiece.Song, error) {
56+
currentlyPlaying, err := c.client.PlayerCurrentlyPlaying(context.Background())
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
if currentlyPlaying.Playing {
62+
return &lyricpiece.Song{
63+
Title: currentlyPlaying.Item.Name,
64+
Artist: currentlyPlaying.Item.Artists[0].Name,
65+
}, nil
66+
}
67+
68+
return nil, nil
69+
}
70+
71+
func (c *Client) queryDBForToken() (*oauth2.Token, error) {
72+
var token *oauth2.Token
73+
err := c.db.View(func(tx *bolt.Tx) error {
74+
bucket := tx.Bucket([]byte("spotify"))
75+
if bucket == nil {
76+
return nil
77+
}
78+
79+
tokenJSON := bucket.Get([]byte("token"))
80+
if tokenJSON == nil {
81+
return nil
82+
}
83+
84+
return json.Unmarshal(tokenJSON, &token)
85+
})
86+
if err != nil {
87+
return nil, err
88+
}
89+
90+
return token, nil
91+
}
92+
93+
func (c *Client) auth() error {
94+
// try to get the token from the database
95+
token, err := c.queryDBForToken()
96+
if err != nil {
97+
return err
98+
}
99+
100+
// if we don't have a token, get one
101+
if token == nil {
102+
url := auth.AuthURL(state)
103+
log.Println("Please log in to Spotify by visiting the following page in your browser:", url)
104+
105+
// first start an HTTP server
106+
http.HandleFunc("/callback", completeAuth)
107+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
108+
log.Println("Got request for:", r.URL.String())
109+
})
110+
go func() {
111+
err := http.ListenAndServe(":8080", nil)
112+
if err != nil {
113+
log.Fatal(err)
114+
}
115+
}()
116+
117+
// wait for auth to complete
118+
token = <-ch
119+
120+
// store the token in bolt
121+
err := c.db.Update(func(tx *bolt.Tx) error {
122+
bucket, err := tx.CreateBucketIfNotExists([]byte("spotify"))
123+
if err != nil {
124+
return fmt.Errorf("create bucket: %s", err)
125+
}
126+
127+
// marshal the token to json
128+
tokenJSON, err := json.Marshal(token)
129+
if err != nil {
130+
return fmt.Errorf("marshal token: %s", err)
131+
}
132+
133+
err = bucket.Put([]byte("token"), []byte(tokenJSON))
134+
if err != nil {
135+
return fmt.Errorf("put token: %s", err)
136+
}
137+
138+
return nil
139+
})
140+
if err != nil {
141+
return err
142+
}
143+
}
144+
145+
// use the token to get an authenticated client
146+
client := spot.New(auth.Client(context.Background(), token))
147+
c.client = client
148+
149+
return nil
150+
}
151+
152+
func completeAuth(w http.ResponseWriter, r *http.Request) {
153+
tok, err := auth.Token(r.Context(), state, r)
154+
if err != nil {
155+
http.Error(w, "Couldn't get token", http.StatusForbidden)
156+
log.Fatal(err)
157+
}
158+
if st := r.FormValue("state"); st != state {
159+
http.NotFound(w, r)
160+
log.Fatalf("State mismatch: %s != %s\n", st, state)
161+
}
162+
163+
ch <- tok
164+
}

vendor/github.com/andybrewer/mack/LICENSE

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)