The full code can be found here:
https://github.com/raymond-design/kpop-cli
Intro:
We'll be learning how to use the Gorilla websockets websocket client and faiface/beep to stream K-pop music πΉ
Setup the project:
Let's first init our Go project by running:
go mod init [project name]
We'll need to download two libraries:
Beep:
go get github.com/faiface/beep
Gorilla Websocket:
go get github.com/gorilla/websocket
Start coding!
It will be helpful if we first setup our project structure.
First create a main.go
file at in our project directory. This will be our entrypoint.
Then create 4 more directories:
connect
play
types
ui
The project structure should look something like this (I also recommend creating a .gitignore
if you plan on pushing to git:
User Interface
Let's first create a file inside the ui
folder. We name the file ui.go
.
This file will define a function that prints song info the terminal! First let's import the "fmt"
package:
package ui
import (
"fmt"
)
Now let's create a function named WriteToFunction
. Make sure to capitalize the first letter (since we'll use it elsewhere):
func WriteToScreen(name string, author string, album string) {
fmt.Print("\033[H\033[2J")
fmt.Println("Now Playing:")
fmt.Println("Title: " + name)
fmt.Println("Artist: " + author)
if album != "" {
fmt.Println("Album: " + album)
}
}
ui.go
looks like this:
Define Types
A helpful pattern in Go is to define related struct types in one place. Let's create a types.go
file in the types directory.
The song info will be in json
format. First import that:
package types
import "encoding/json"
Next, we need to describe some types for WebSockets connection:
type SocketRes struct {
Op int64 `json:"op"`
D json.RawMessage
}
type SendData struct {
Op int64 `json:"op"`
}
type HeartbeatData struct {
Message string `json:"message"`
Heartbeat int64 `json:"heartbeat"`
}
Next, we will define some structs related to the songs themselves(Song, Album, etc.):
type PlayingData struct {
Song Song `json:"song"`
Requester interface{} `json:"requester"`
Event interface{} `json:"event"`
StartTime string `json:"startTime"`
LastPlayed []Song `json:"lastPlayed"`
Listeners int64 `json:"listeners"`
}
type Song struct {
ID int64 `json:"id"`
Title string `json:"title"`
Sources []interface{} `json:"sources"`
Artists []Album `json:"artists"`
Albums []Album `json:"albums"`
Duration int64 `json:"duration"`
}
type Album struct {
ID int64 `json:"id"`
Name string `json:"name"`
NameRomaji *string `json:"nameRomaji"`
Image *string `json:"image"`
}
Now that we finished definitions, we can create the client to stream audio!
Create the WebSocket Client π
Head over to the connect
directory and create a connect.go
file.
In this package, we'll need to import Gorilla websocket and the two packages we've already created:
package connect
import (
"encoding/json"
"log"
"time"
"github.com/raymond-design/kpop-cli/types"
"github.com/raymond-design/kpop-cli/ui"
"github.com/gorilla/websocket"
)
We also need to define 3 package-level variables:
var conn *websocket.Conn
var done = false
var ticker *time.Ticker
Let's a create a function to initialize the connection:
func Start(url string) {
}
(Later on, url string
will be the WebSocket server url that we want to stream from)
Now paste the following:
conn_l, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
log.Fatal("Couldn't connect to websocket")
}
conn = conn_l
If the conn doesn't work, there could be an error with the URL!
Now, let's run an anonymous function Goroutine to maintain the WebSocket connection:
go func() {
for {
if done {
conn.Close()
break
}
_, msg, err := conn.ReadMessage()
if err != nil {
log.Fatal("Couldn't read websocket message")
}
handleMessage(msg)
}
}()
We will keep on maintaining the connection until program break or a read error. The function should look something like this:
Now we need to implement that handleMessage
function!
func handleMessage(in []byte) {
var msg types.SocketRes
json.Unmarshal(in, &msg)
switch msg.Op {
case 0:
var data types.HeartbeatData
json.Unmarshal(msg.D, &data)
setHeartbeat(data.Heartbeat)
case 1:
var data types.PlayingData
json.Unmarshal(msg.D, &data)
album := "None"
if len(data.Song.Albums) > 0 {
album = data.Song.Albums[0].Name
}
ui.WriteToScreen(data.Song.Title, data.Song.Artists[0].Name, album)
}
}
In the start function, we continually call this function which will grab the current song data and print it.
To make the code cleaner, the actual set heartbeat logic will be in two other functions:
func sendHeartBeat() {
data := types.SendData{
Op: 9,
}
conn.WriteJSON(data)
}
func setHeartbeat(repeat int64) {
sendHeartBeat()
ticker = time.NewTicker(time.Duration(repeat) * time.Millisecond)
go func() {
<-ticker.C
sendHeartBeat()
}()
}
If you want to read more about WebSockets connections, here's a helpful article:
https://www.programmerall.com/article/821816187/
Finally, we just need a stopping function that will break out of that for loop:
func Stop() {
ticker.Stop()
done = true
}
Now that we have these WebSockets connection functions, we can bring sound into the app!
Including Sound!
To bring in sound, we will be importing faiface/beep
:
package play
import (
"log"
"net/http"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
)
We will also create a global var from this beep package:
var stream beep.StreamSeekCloser
We will need two functions. One to play and one to stop.
The play function is quite simple. We will check the validity of the http url and then use the beep/mp3
to starting streaming audio contents!
func Play(url string) {
resp, err := http.Get(url)
if err != nil {
log.Fatal("http error")
}
l_streamer, format, err := mp3.Decode(resp.Body)
stream = l_streamer
if err != nil {
log.Fatal("decoding error")
}
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
speaker.Play(stream)
}
The stop function is even simpler. We just close the stream:
func Stop() {
stream.Close()
}
The code looks something like this:
Project Entrypoint
Now we can create the entrypoint to our app! Let's import our packages:
package main
import (
"fmt"
"os"
"os/signal"
"github.com/raymond-design/kpop-cli/connect"
"github.com/raymond-design/kpop-cli/play"
)
Now let's define the server URL that we'll stream from:
const JPOP string = "https://listen.moe/fallback"
const KPOP string = "https://listen.moe/kpop/fallback"
const JSOCKET string = "wss://listen.moe/gateway_v2"
const KSOCKET string = "wss://listen.moe/kpop/gateway_v2"
By the way, you can also stream J-pop music now!
Now create the main function:
func main(){
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
mode := "kpop"
var stream string
var socket string
if len(os.Args) == 2 {
mode = os.Args[1]
}
}
We can use a switch function to switch between K-pop and J-pop music:
switch mode {
case "kpop":
stream = KPOP
socket = KSOCKET
case "jpop":
stream = JPOP
socket = JSOCKET
default:
fmt.Println("Error")
os.Exit(1)
}
Now, we can connect and start streaming music!
connect.Start(socket)
play.Play(stream)
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
<-interrupt
fmt.Println("Exiting Player")
play.Stop()
connect.Stop()
(Notice we stop first stop decoding audio, then disconnect from the WebSockets server)
The main function looks like this:
Listening to the radio πΆπ€
- Run a
go get
to get all dependencies. - Run
go build .
in the project. - Run
./kpop-cli kpop
to play K-pop music or./kpop-cli jpop
(If you implemented J-pop).
Now you know how to implement sound and WebSocket streaming in Go!
Also try streaming other types of data in the future π
The full code can be found here:
https://github.com/raymond-design/kpop-cli
Top comments (0)