Skip to content

Commit

Permalink
Merge pull request #7 from raitonoberu/dev
Browse files Browse the repository at this point in the history
Improved codebase. Customization & piping.
  • Loading branch information
raitonoberu authored Feb 26, 2022
2 parents d55b5cd + d9f2ef2 commit 8c7a48b
Show file tree
Hide file tree
Showing 13 changed files with 728 additions and 412 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Timesynced lyrics in your terminal.
- Fully compatible with [spotifyd](https://github.com/Spotifyd/spotifyd).
- Works well with long lines & Unicode characters.
- Easy to use customization.
- Single binary & cross-plaftorm.

## Installation
Expand Down Expand Up @@ -59,17 +60,21 @@ Since Spotify requires a special web token to display song lyrics, you need to s
4. Scroll down to the `Request Headers`, right click the `cookie` field and select `Copy value`.
5. Paste it when you are asked.
Another way to set cookie is to set the `SPOTIFY_COOKIE` enviroment variable. You can always clear cookie by running `sptlrx --clear`.
You can also set the `SPOTIFY_COOKIE` enviroment variable or pass the `--cookie` flag, and your cookie will be saved on the next run. You can always clear cookie by running `sptlrx clear`.
## Information
### In development
### Styling
`sptlrx` is pretty much ready to use, however you may encounter some bugs. Please open a new issue so that we can fix it quickly. Also, I plan to add additional settings, such as PC-like mode, color text and more. Stay tuned 😉
There are three special flags for applying custom colors and styles to lines: `--current`, `--before` and `--after`. The syntax for all flags is the same - pass styles and colors separated by commas. Example:
```sh
sptlrx --current "bold,#FFDFD3,#957DAD" --before "104,faint,italic" --after "104,faint"
```
List of allowed styles: `bold`, `italic`, `underline`, `strikethrough`, `blink`, `faint`. The colors can be either in HEX format, or ANSI 0-255. The first color represents the foreground, the second represents the background. **Note that styles will not work if your terminal does not support them.**

### Delay
### Piping

For some reason unknown to me, there is a delay in the lyrics on some devices. You can manually adjust the delay by using the "**+**" and "**-**" symbols on the keyboard (adds or subtracts 100 ms).
Run `sptlrx pipe` to start printing the current lines in stdout. This can be used in various status bars and other applications. You can specify the maximum line length and overflow (run `sptlrx pipe -h` for help).

## License

Expand Down
26 changes: 26 additions & 0 deletions cmd/clear.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package cmd

import (
"errors"
"fmt"
"os"
"sptlrx/cookie"

"github.com/muesli/coral"
)

var clearCmd = &coral.Command{
Use: "clear",
Short: "Clear saved cookie",

RunE: func(cmd *coral.Command, args []string) error {
err := cookie.Clear()
if err == nil {
fmt.Println("Cookie have been cleared.")
} else if errors.Is(err, os.ErrNotExist) {
fmt.Println("You haven't saved any cookies 🍪")
return nil
}
return err
},
}
97 changes: 97 additions & 0 deletions cmd/pipe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package cmd

import (
"errors"
"fmt"
"os"
"sptlrx/cookie"
"sptlrx/pool"
"sptlrx/spotify"
"strings"

"github.com/muesli/coral"
"github.com/muesli/reflow/wordwrap"
"github.com/muesli/reflow/wrap"
)

var (
FlagLength int
FlagOverflow string
FlagIgnoreErrors bool
)

var pipeCmd = &coral.Command{
Use: "pipe",
Short: "Pipes the current line to stdout",

RunE: func(cmd *coral.Command, args []string) error {
var clientCookie string

if FlagCookie != "" {
clientCookie = FlagCookie
} else if envCookie := os.Getenv("SPOTIFY_COOKIE"); envCookie != "" {
clientCookie = envCookie
} else {
clientCookie, _ = cookie.Load()
}

if clientCookie == "" {
return errors.New("couldn't find cookie")
}

client, err := spotify.NewClient(clientCookie)
if err != nil {
return fmt.Errorf("couldn't create client: %w", err)
}
if err := cookie.Save(clientCookie); err != nil {
return fmt.Errorf("couldn't save cookie: %w", err)
}

ch := make(chan pool.Update)
go pool.Listen(client, ch)

for update := range ch {
if update.Err != nil {
if !FlagIgnoreErrors {
fmt.Println(err.Error())
}
continue
}
if update.Lines == nil || !update.Lines.Timesynced() {
fmt.Println("")
continue
}
line := update.Lines[update.Index].Words
if FlagLength == 0 {
fmt.Println(line)
} else {
// TODO: find out if there is a better way to cut the line
switch FlagOverflow {
case "word":
s := wordwrap.String(line, FlagLength)
fmt.Println(strings.Split(s, "\n")[0])
case "none":
s := wrap.String(line, FlagLength)
fmt.Println(strings.Split(s, "\n")[0])
case "ellipsis":
s := wrap.String(line, FlagLength)
lines := strings.Split(s, "\n")
if len(lines) == 1 {
fmt.Println(lines[0])
} else {
s := wrap.String(lines[0], FlagLength-3)
fmt.Println(strings.Split(s, "\n")[0] + "...")
}
}
}
}
return nil
},
}

func init() {
pipeCmd.Flags().StringVar(&FlagCookie, "cookie", "", "your cookie")
pipeCmd.Flags().IntVar(&FlagLength, "length", 0, "max length of line")
pipeCmd.Flags().StringVar(&FlagOverflow, "overflow", "word", "how to wrap an overflowed line (none/word/ellipsis)")
pipeCmd.Flags().BoolVar(&FlagIgnoreErrors, "ignore-errors", false, "don't print errors")
}
193 changes: 193 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package cmd

import (
"bufio"
"fmt"
"log"
"os"
"sptlrx/cookie"
"sptlrx/spotify"
"sptlrx/ui"
"strconv"
"strings"

tea "github.com/charmbracelet/bubbletea"
gloss "github.com/charmbracelet/lipgloss"
"github.com/muesli/coral"
)

const banner = `
_ _
___ _ __ | |_ | | _ __ __ __
/ __|| '_ \ | __|| || '__|\ \/ /
\__ \| |_) || |_ | || | > <
|___/| .__/ \__||_||_| /_/\_\
|_|
`

const help = `
How to get setup:
1. Open your browser.
2. Press F12, open the 'Network' tab and go to open.spotify.com.
3. Click on the first request to open.spotify.com.
4. Scroll down to the 'Request Headers', right click the 'cookie' field and select 'Copy value'.
`

var (
FlagCookie string

FlagStyleBefore string
FlagStyleCurrent string
FlagStyleAfter string

FlagHAlignment string
)

var rootCmd = &coral.Command{
Use: "sptlrx",
Short: "Spotify lyrics in your terminal",
Long: "A CLI app that shows time-synced Spotify lyrics in your terminal.",
Version: "v0.1.0",
SilenceUsage: true,

RunE: func(cmd *coral.Command, args []string) error {
var clientCookie string

if FlagCookie != "" {
clientCookie = FlagCookie
} else if envCookie := os.Getenv("SPOTIFY_COOKIE"); envCookie != "" {
clientCookie = envCookie
} else {
fileCookie, err := cookie.Load()
if err != nil {
return fmt.Errorf("couldn't load cookie: %w", err)
}
clientCookie = fileCookie
}

if clientCookie == "" {
fmt.Print(banner)
fmt.Printf("Cookie will be stored in %s\n", cookie.Directory)
fmt.Print(help)
ask("Enter your cookie:", &clientCookie)
fmt.Println("You can always clear cookie by running 'sptlrx clear'.")
}

client, err := spotify.NewClient(clientCookie)
if err != nil {
return fmt.Errorf("couldn't create client: %w", err)
}
if err := cookie.Save(clientCookie); err != nil {
return fmt.Errorf("couldn't save cookie: %w", err)
}

hAlignment := 0.5
switch FlagHAlignment {
case "left":
hAlignment = 0
case "right":
hAlignment = 1
}

p := tea.NewProgram(
&ui.Model{
Client: client,
HAlignment: gloss.Position(hAlignment),
StyleBefore: parseStyle(FlagStyleBefore),
StyleCurrent: parseStyle(FlagStyleCurrent),
StyleAfter: parseStyle(FlagStyleAfter),
},
tea.WithAltScreen(),
)
return p.Start()
},
}

func ask(what string, answer *string) {
var ok bool
scanner := bufio.NewScanner(os.Stdin)
for !ok {
fmt.Println("\n" + what)
scanner.Scan()

if err := scanner.Err(); err != nil {
log.Fatal(err)
}

line := strings.TrimSpace(scanner.Text())

if line != "" {
ok = true
*answer = line
} else {
fmt.Println("The value can't be empty.")
}
}
}

func parseStyle(value string) gloss.Style {
var style gloss.Style

if value == "" {
return style
}

for _, part := range strings.Split(value, ",") {
switch part {
case "bold":
style = style.Bold(true)
case "italic":
style = style.Italic(true)
case "underline":
style = style.Underline(true)
case "strikethrough":
style = style.Strikethrough(true)
case "blink":
style = style.Blink(true)
case "faint":
style = style.Faint(true)
default:
if validateColor(part) {
if style.GetForeground() == (gloss.NoColor{}) {
style = style.Foreground(gloss.Color(part))
} else {
style = style.Background(gloss.Color(part))
style.ColorWhitespace(false)
}
} else {
fmt.Println("Invalid style:", part)
}
}
}
return style
}

func validateColor(color string) bool {
if _, err := strconv.Atoi(color); err == nil {
return true
}
if strings.HasPrefix(color, "#") {
return true
}
return false
}

func init() {
rootCmd.Flags().StringVar(&FlagCookie, "cookie", "", "your cookie")

rootCmd.Flags().StringVar(&FlagStyleBefore, "before", "bold", "style of the lines before the current ones")
rootCmd.Flags().StringVar(&FlagStyleCurrent, "current", "bold", "style of the current lines")
rootCmd.Flags().StringVar(&FlagStyleAfter, "after", "faint", "style of the lines after the current ones")

rootCmd.Flags().StringVar(&FlagHAlignment, "halign", "center", "initial horizontal alignment (left/center/right)")

rootCmd.AddCommand(clearCmd)
rootCmd.AddCommand(pipeCmd)
}

func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
Loading

0 comments on commit 8c7a48b

Please sign in to comment.