di-tui/app/app.go
2020-05-09 14:18:06 -04:00

155 lines
4.1 KiB
Go

package app
import (
"bytes"
"errors"
"fmt"
"image"
"image/color"
_ "image/jpeg"
"io/ioutil"
"net/http"
"reflect"
"time"
"github.com/acaloiaro/di-tui/components"
"github.com/acaloiaro/di-tui/context"
"github.com/acaloiaro/di-tui/difm"
"github.com/faiface/beep/speaker"
"github.com/michiwend/gomusicbrainz"
"github.com/nfnt/resize"
)
// Art fetches album art for the given track, converts it to ASCII, and return the ASCII stringified album art
func Art(artist, track string) (art string) {
// create a new WS2Client.
client, err := gomusicbrainz.NewWS2Client(
"https://musicbrainz.org/ws/2",
"di-tui",
"0.0.1",
"http://github.com/acaloiaro/di-tui")
if err != nil {
return
}
// search musicbrainz for a matching artist/track
searchString := fmt.Sprintf(`artist:"%s" release:"%s"`, artist, track)
mbResp, _ := client.SearchRelease(searchString, 1, -1)
if len(mbResp.Releases) == 0 {
err = fmt.Errorf("no releases foudn for the artist (%s) and track (%s)", artist, track)
return
}
// fetch the album art and convert it to ascii
release := mbResp.Releases[0]
url := fmt.Sprintf("http://coverartarchive.org/release/%s/front", release.ID)
resp, err := http.Get(url)
if err != nil {
return
}
img, format, err := image.Decode(resp.Body)
if format != "jpeg" && format != "png" || err != nil {
err = errors.New("only jpeg and png images supported")
return
}
// TODO: Consider the current window size when calculating width
art = string(convertToAscii(scaleImage(img, 40)))
return
}
func scaleImage(img image.Image, w int) (image.Image, int, int) {
sz := img.Bounds()
h := (sz.Max.Y * w * 10) / (sz.Max.X * 16)
img = resize.Resize(uint(w), uint(h), img, resize.Lanczos3)
return img, w, h
}
func convertToAscii(img image.Image, w, h int) []byte {
var ASCIISTR = "MND8OZ$7I?+=~:,.."
table := []byte(ASCIISTR)
buf := new(bytes.Buffer)
for i := 0; i < h; i++ {
for j := 0; j < w; j++ {
g := color.GrayModel.Convert(img.At(j, i))
y := reflect.ValueOf(g).FieldByName("Y").Uint()
pos := int(y * 16 / 255)
_ = buf.WriteByte(table[pos])
}
_ = buf.WriteByte('\n')
}
return buf.Bytes()
}
// PlayChannel begins streaming the provided channel after fetching its playlist
// If a channel is already playing, the old stream is stopped first, clearing up resources.
// This function is *asynchronous* and creates a single streaming resource: the audio stream held by the application
// context. To clean up resources created by this function, Close() the application's audio stream.
func PlayChannel(chn *components.ChannelItem, ctx *context.AppContext) {
// when other channels are already playing, close their stream before playing a new one
if ctx.AudioStream != nil {
ctx.AudioStream.Close()
}
go func() {
client := &http.Client{}
req, _ := http.NewRequest("GET", chn.Playlist, nil)
resp, err := client.Do(req)
if err != nil || resp.StatusCode != 200 {
ctx.SetStatusMessage(fmt.Sprintf("Unable to stream channel: %s", chn.Name))
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if streamURL, ok := difm.GetStreamURL(body, ctx); ok {
format := difm.Stream(streamURL, ctx)
if !ctx.SpeakerInitialized {
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
ctx.SpeakerInitialized = true
}
speaker.Play(ctx.AudioStream)
ctx.IsPlaying = true
UpdateNowPlaying(chn, ctx)
}
}()
}
// TogglePause pauses/unpauses audio when a channel is playing
func TogglePause(ctx *context.AppContext) {
// nothing to do if nothing has been streamed
if ctx.AudioStream == nil {
return
}
ctx.AudioStream.Close()
if !ctx.IsPlaying {
PlayChannel(ctx.CurrentChannel, ctx)
}
ctx.IsPlaying = !ctx.IsPlaying
}
// UpdateNowPlaying updates the application's now playing view with the currently playing channel
func UpdateNowPlaying(chn *components.ChannelItem, ctx *context.AppContext) {
ctx.CurrentChannel = chn
cp := difm.GetCurrentlyPlaying(ctx)
albumArt := Art(cp.Track.Artist, cp.Track.Title)
ctx.View.App.QueueUpdateDraw(func() {
ctx.View.NowPlaying.Channel = chn
ctx.View.NowPlaying.Track = cp.Track
ctx.View.NowPlaying.Art = albumArt
})
}