Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add spotify widget #125

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,25 @@ corresponding icons with correct names need to be placed in
`~/.local/share/deckmaster/themes/[theme]`. The default icons with their
respective names can be found [here](https://github.com/muesli/deckmaster/tree/master/assets/weather).

#### Spotify

A widget that displays the current album and track position.

```toml
[keys.widget]
id = "spotify"
interval = 500
[keys.widget.config]
fontsize = 8
color = "#ffffff"

```

This widget depends on the [spotifycli](https://github.com/pwittchen/spotify-cli-linux) application for Linux.

The widget will display the album along with the track position of the current song. The album art is loaded directly
from the url provided by the spotify via the spotifycli cli.

### Actions

You can hook up any key with several actions. A regular keypress will trigger
Expand Down
Binary file added assets/spotify/off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/spotify/on.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/spotify/pause.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions decks/spotify.deck
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[[keys]]
index = 0
[keys.widget]
id = "button"
[keys.widget.config]
fontsize = 8
label = "<<"
[keys.action]
exec = "spotifycli --prev"

[[keys]]
index = 1
[keys.widget]
id = "spotify"
interval = 500
[keys.widget.config]
fontsize = 8
[keys.action]
exec = "spotifycli --playpause"

[[keys]]
index = 2
[keys.widget]
id = "button"
[keys.widget.config]
fontsize = 8
label = ">>"
[keys.action]
exec = "spotifycli --next"
3 changes: 3 additions & 0 deletions widget.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ func NewWidget(dev *streamdeck.Device, base string, kc KeyConfig, bg image.Image

case "weather":
return NewWeatherWidget(bw, kc.Widget)

case "spotify":
return NewSpotifyWidget(bw, kc.Widget)
}

// unknown widget ID
Expand Down
175 changes: 175 additions & 0 deletions widget_spotify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package main

import (
"errors"
"fmt"
"github.com/nfnt/resize"
"image"
"image/color"
"image/draw"
"net/http"
"strings"
"time"
)

// SpotifyWidget is a widget that displays album art and current track position
type SpotifyWidget struct {
*BaseWidget

fontsize float64
color color.Color
flatten bool
}

// NewSpotifyWidget returns a new SpotifyWidget.
func NewSpotifyWidget(bw *BaseWidget, opts WidgetConfig) (*SpotifyWidget, error) {
bw.setInterval(time.Duration(opts.Interval)*time.Millisecond, 0)

var fontsize float64
_ = ConfigValue(opts.Config["fontsize"], &fontsize)
var color color.Color
_ = ConfigValue(opts.Config["color"], &color)
var flatten bool
_ = ConfigValue(opts.Config["flatten"], &flatten)

if color == nil {
color = DefaultColor
}

w := &SpotifyWidget{
BaseWidget: bw,
fontsize: fontsize,
color: color,
flatten: flatten,
}

return w, nil
}

// Parse a string like (00:00/00:00) and return the first set of numbers 00:00
func extractFirstTimeSection(input string) (string, error) {
// Remove the parentheses and split the string into two parts
parts := strings.Split(strings.Trim(input, "()"), "/")

// Check if the input string has the correct format
if len(parts) != 2 {
return "", fmt.Errorf("invalid input format")
}

// Return the first time section
return parts[0], nil
}

// LoadImage loads an image from disk.
func (w *SpotifyWidget) LoadImage(path string) (image.Image, error) {
path, err := expandPath(w.base, path)
if err != nil {
return nil, err
}
icon, err := loadImage(path)
if err != nil {
return nil, err
}

return icon, nil
}

// LoadImageFromURL loads an image from a URL
func (w *SpotifyWidget) LoadImageFromURL(URL string) (image.Image, error) {
//Get the response bytes from the url
response, err := http.Get(URL)
if err != nil {
return nil, err
}
defer response.Body.Close()

if response.StatusCode != 200 {
return nil, errors.New("received non 200 response code")
}

img, _, err := image.Decode(response.Body)
if err != nil {
return nil, err
}

return img, err
}

// Update renders the widget.
func (w *SpotifyWidget) Update() error {
size := int(w.dev.Pixels)
margin := size / 18
height := size - (margin * 2)
img := image.NewRGBA(image.Rect(0, 0, size, size))

status, err := runCommand("spotifycli --playbackstatus")
if err != nil {
icon, err := loadImage("assets/spotify/off.png")
if err != nil {
return err
}
drawImage(img, icon, size, image.Pt(0, 0))
return w.render(w.dev, img)
}

position, err := runCommand("spotifycli --position")
if err != nil {
return err
}

url, err := runCommand("spotifycli --arturl")
if err != nil {
return err
}

icon, err := w.LoadImageFromURL(url)
if err != nil {
return err
}

iconsize := int((float64(height) / 3.0) * 2.0)
bounds := img.Bounds()

// spotifycli --position returns a string with the following format
// (00:00/00:00), so we need to extract the first part to get the
// current playback position
position, err = extractFirstTimeSection(position)
if err != nil {
return err
}

if icon != nil {

icon = resize.Resize(uint(size), uint(size), icon, resize.Bilinear)
draw.Draw(img, img.Bounds(), icon, image.Pt(0, margin), draw.Over)

if err != nil {
return err
}

if status == string("▮▮") {
pauseIcon, err := loadImage("assets/spotify/pause.png")
pauseIcon = resize.Resize(uint(size), uint(size), pauseIcon, resize.Bilinear)
draw.Draw(img, img.Bounds(), pauseIcon, image.Pt(0, margin), draw.Over)

if err != nil {
return err
}

}

bounds.Min.Y += iconsize + margin
bounds.Max.Y -= margin
}

drawString(img,
bounds,
ttfFont,
position,
w.dev.DPI,
w.fontsize,
w.color,
image.Pt(-1, -1))

return w.render(w.dev, img)
}