Skip to content

Commit

Permalink
Merge pull request #3 from kylegrantlucas/spotify
Browse files Browse the repository at this point in the history
  • Loading branch information
kylegrantlucas authored Oct 1, 2022
2 parents a259856 + 9d9e8ee commit d5367a8
Show file tree
Hide file tree
Showing 233 changed files with 56,030 additions and 813 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ A small tool for my command line, gets the currently playing spotify track and t

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

## Setup

Using this cli tool requires setting up a Spotify OAuth2 Application so we can call the API for the user's current info.

1. Register an application at: https://developer.spotify.com/my-applications/
- Use `http://localhost:8080/callback` as the redirect URI

2. Set the `SPOTIFY_ID` environment variable to the client ID you got in step 1.
3. Set the `SPOTIFY_SECRET` environment variable to the client secret from step 1.

## Usage

`$ lyricpiece`
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ module github.com/kylegrantlucas/lyricpiece
go 1.19

require (
github.com/andybrewer/mack v0.0.0-20220307193339-22e922cc18af
github.com/boltdb/bolt v1.3.1
github.com/rhnvrm/lyric-api-go v0.1.4
github.com/zmb3/spotify/v2 v2.3.0
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5
)

require (
github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gosimple/slug v1.13.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
)
379 changes: 377 additions & 2 deletions go.sum

Large diffs are not rendered by default.

51 changes: 24 additions & 27 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ package main
import (
"fmt"
"os"
"sync"

"github.com/andybrewer/mack"
"github.com/kylegrantlucas/lyricpiece/lyricpiece"
"github.com/kylegrantlucas/lyricpiece/spotify"
)

func main() {
Expand All @@ -15,19 +14,37 @@ func main() {
fmt.Fprintln(os.Stderr, err)
}

client, err := lyricpiece.NewClient(dbPath)
// call to spotify to get the currently playing song
spotClient, err := spotify.NewClient(dbPath)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}

song, err := spotClient.GetCurrentlyPlaying()
if err != nil {
fmt.Fprintln(os.Stderr, err)
}

// close up the DB so the lyricpiece client can use it
err = spotClient.Close()
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
defer client.Close()

song := getCurrentSong()
lyricPiece, err := client.GetLyricPiece(song)
client, err := lyricpiece.NewClient(dbPath)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
defer client.Close()

if song != nil {
lyricPiece, err := client.GetLyricPiece(*song)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}

fmt.Print(lyricPiece)
fmt.Print(lyricPiece)
}
}

func buildDBPath() (string, error) {
Expand All @@ -44,23 +61,3 @@ func buildDBPath() (string, error) {

return path + "/lyricpiece.db", nil
}

func getCurrentSong() lyricpiece.Song {
var track, artist string
wg := &sync.WaitGroup{}
wg.Add(2)

go func() {
track, _ = mack.Tell("Spotify", "name of current track as string")
wg.Done()
}()

go func() {
artist, _ = mack.Tell("Spotify", "artist of current track as string")
wg.Done()
}()

wg.Wait()

return lyricpiece.Song{Title: track, Artist: artist}
}
164 changes: 164 additions & 0 deletions spotify/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// 1. Register an application at: https://developer.spotify.com/my-applications/
// - Use "http://localhost:8080/callback" as the redirect URI

// 2. Set the SPOTIFY_ID environment variable to the client ID you got in step 1.
// 3. Set the SPOTIFY_SECRET environment variable to the client secret from step 1.
package spotify

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"

"github.com/boltdb/bolt"
"github.com/kylegrantlucas/lyricpiece/lyricpiece"
spot "github.com/zmb3/spotify/v2"
spotifyauth "github.com/zmb3/spotify/v2/auth"
"golang.org/x/oauth2"
)

const redirectURI = "http://localhost:8080/callback"

var (
auth = spotifyauth.New(spotifyauth.WithRedirectURL(redirectURI), spotifyauth.WithScopes(spotifyauth.ScopeUserReadCurrentlyPlaying, spotifyauth.ScopeUserReadPlaybackState))
ch = make(chan *oauth2.Token)
state = "abc123"
)

type Client struct {
db *bolt.DB
client *spot.Client
}

func NewClient(dbPath string) (*Client, error) {
db, err := bolt.Open(dbPath, 0600, nil)
if err != nil {
return nil, err
}

client := &Client{
db: db,
}

client.auth()

return client, nil
}

func (c *Client) Close() error {
return c.db.Close()
}

// get currently playing song
func (c *Client) GetCurrentlyPlaying() (*lyricpiece.Song, error) {
currentlyPlaying, err := c.client.PlayerCurrentlyPlaying(context.Background())
if err != nil {
return nil, err
}

if currentlyPlaying.Playing {
return &lyricpiece.Song{
Title: currentlyPlaying.Item.Name,
Artist: currentlyPlaying.Item.Artists[0].Name,
}, nil
}

return nil, nil
}

func (c *Client) queryDBForToken() (*oauth2.Token, error) {
var token *oauth2.Token
err := c.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte("spotify"))
if bucket == nil {
return nil
}

tokenJSON := bucket.Get([]byte("token"))
if tokenJSON == nil {
return nil
}

return json.Unmarshal(tokenJSON, &token)
})
if err != nil {
return nil, err
}

return token, nil
}

func (c *Client) auth() error {
// try to get the token from the database
token, err := c.queryDBForToken()
if err != nil {
return err
}

// if we don't have a token, get one
if token == nil {
url := auth.AuthURL(state)
log.Println("Please log in to Spotify by visiting the following page in your browser:", url)

// first start an HTTP server
http.HandleFunc("/callback", completeAuth)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("Got request for:", r.URL.String())
})
go func() {
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}()

// wait for auth to complete
token = <-ch

// store the token in bolt
err := c.db.Update(func(tx *bolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte("spotify"))
if err != nil {
return fmt.Errorf("create bucket: %s", err)
}

// marshal the token to json
tokenJSON, err := json.Marshal(token)
if err != nil {
return fmt.Errorf("marshal token: %s", err)
}

err = bucket.Put([]byte("token"), []byte(tokenJSON))
if err != nil {
return fmt.Errorf("put token: %s", err)
}

return nil
})
if err != nil {
return err
}
}

// use the token to get an authenticated client
client := spot.New(auth.Client(context.Background(), token))
c.client = client

return nil
}

func completeAuth(w http.ResponseWriter, r *http.Request) {
tok, err := auth.Token(r.Context(), state, r)
if err != nil {
http.Error(w, "Couldn't get token", http.StatusForbidden)
log.Fatal(err)
}
if st := r.FormValue("state"); st != state {
http.NotFound(w, r)
log.Fatalf("State mismatch: %s != %s\n", st, state)
}

ch <- tok
}
21 changes: 0 additions & 21 deletions vendor/github.com/andybrewer/mack/LICENSE

This file was deleted.

Loading

0 comments on commit d5367a8

Please sign in to comment.