Skip to content
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
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ nix-env -iA nixos.sptlrx
nix-env -iA nixpkgs.sptlrx
```

- Fedora ([@yuliusw](https://github.com/yuliusw))

```sh
sudo dnf copr enable yuliusw/sptlrx
sudo dnf install sptlrx
#some Fedora release do not support latest version of GO . if you want latest ,use
# @go-sig/golang-rawhide-dwarf5 copr to build
```

**Windows**, **MacOS** & **Other**

Download the binary from the [Releases](https://github.com/raitonoberu/sptlrx/releases/latest) page or [build it yourself](./building.md).
Expand All @@ -54,7 +63,10 @@ Config file will be created at the first launch. On Linux it's located in `~/.co

```yaml
### Global settings ###
# Player that will be used. Possible values: spotify, mpd, mopidy, mpris.
# Player that will be used. Possible values: spotify, mpd, mopidy, mpris, browser.
# You can also provide a comma-separated list to try players in order, e.g.:
# player: mopidy ,mpris
# Only the players explicitly listed will be tried, in the given order.
player: spotify
# Whether to ignore errors instead of showing them.
ignoreErrors: true
Expand Down Expand Up @@ -190,6 +202,19 @@ mpris:

Linux only. System player that supports MPRIS protocol will be used. You can also specify a whitelist of players to use, example: `players: [rhythmbox, spotifyd, ncspot]`. Run `playerctl -l` to get the names.

### Multiple Players (fallback by order)

```yaml
# config.yaml
player: mopidy ,mpris
mopidy:
address: 127.0.0.1:6680
mpris:
players: []
```

The application will initialize players in the order specified by `player` and use the first one that initializes successfully. Players not listed will not be tried.

### Browser

```yaml
Expand Down
123 changes: 123 additions & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<div align="center">

<h1><a href="https://github.com/raitonoberu/sptlrx">sptlrx</a></h1>
<h4>在终端中显示同步歌词</h4>

<a href="https://www.youtube.com/watch?v=qR2QIJdtgiU">
<img title="demo" src="./demo.gif" width="450"/>
</a>

</div>

## 功能

- 支持 Spotify、MPD、Mopidy、MPRIS 与浏览器扩展。
- 适配长行与 Unicode 字符。
- 易于自定义样式。
- 可将歌词通过管道输出到 stdout。
- 单一二进制,跨平台。

## 安装

Linux、Windows、macOS 等平台可从 [Releases](https://github.com/raitonoberu/sptlrx/releases/latest) 下载,或参见 [building.md](./building.md) 自行编译。

## 配置

首次运行会自动在配置目录创建 `config.yaml`。在 Linux 上路径通常为 `~/.config/sptlrx/config.yaml`。运行 `sptlrx -h` 可查看完整路径。

示例配置(节选):

```yaml
### 全局设置 ###
# 使用的播放器。可选:spotify, mpd, mopidy, mpris, browser
# 也可以使用英文逗号分隔按顺序尝试多个播放器,例如:
# player: mopidy ,mpris
# 程序会按顺序初始化,直到有一个成功为止;未列出的播放器不会被尝试。
player: spotify
# 是否忽略错误(为 true 时不在界面显示错误)。
ignoreErrors: true
# 内部计时器间隔(毫秒),决定终端刷新频率。
timerInterval: 200
# 位置更新间隔(毫秒),不显著影响精度。
updateInterval: 2000

### 样式设置 ###
style:
# 行的水平对齐方式:left, center, right
hAlignment: center
before:
background: ""
foreground: ""
bold: true
italic: false
underline: false
strikethrough: false
blink: false
faint: false
current:
background: ""
foreground: ""
bold: true
italic: false
underline: false
strikethrough: false
blink: false
faint: false
after:
background: ""
foreground: ""
bold: false
italic: false
underline: false
strikethrough: false
blink: false
faint: true

### MPD 设置 ###
mpd:
address: 127.0.0.1:6600
password: ""

### Mopidy 设置 ###
mopidy:
address: 127.0.0.1:6680

### MPRIS 设置 ###
mpris:
# MPRIS 播放器白名单;为空则自动选第一个可用的。
players: []

### 浏览器扩展 ###
browser:
port: 8974

### 本地歌词 ###
local:
# 扫描 .lrc 文件的目录,例如:"~/Music"。设置后将仅使用本地歌词源。
folder: ""
```

### 多播放器顺序(回退)示例

```yaml
# config.yaml
player: mopidy ,mpris
mopidy:
address: 127.0.0.1:6680
mpris:
players: []
```

程序会按照 `player` 字段给定的顺序依次尝试初始化播放器,使用第一个初始化成功的播放器。仅尝试用户列出的播放器。

### Spotify 登录

将 `player` 设为 `spotify` 后,首次使用需要登录:执行 `sptlrx login`,根据提示提供 `Client ID`/`Client Secret`,或通过环境变量 `SPOTIFY_CLIENT_ID`/`SPOTIFY_CLIENT_SECRET` 或命令行参数 `--client-id`/`--client-secret` 提供。登录成功后凭据将保存到 `$XDG_STATE_HOME/sptlrx/spotify-auth.json`。

### 输出到管道

执行 `sptlrx pipe` 可将当前行输出到标准输出,便于与状态栏等其他程序集成。

## 许可证

**MIT License**,详见 [LICENSE](./LICENSE)。
77 changes: 63 additions & 14 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,19 +173,68 @@ func validateColor(color string) bool {
return false
}

// GetPlayer returns a player based on config values
// GetPlayer returns a player based on config values.
// It supports a comma-separated list in conf.Player for backward-compatible multi-player configuration.
// Example: "mopidy ,mpris" will try Mopidy first, then MPRIS if Mopidy init fails.
// Only players explicitly listed by the user will be tried.
func GetPlayer(conf *Config) (player.Player, error) {
switch conf.Player {
case "spotify":
return spotify.New()
case "mpd":
return mpd.New(conf.Mpd.Address, conf.Mpd.Password), nil
case "mopidy":
return mopidy.New(conf.Mopidy.Address), nil
case "mpris":
return mpris.New(conf.Mpris.Players)
case "browser":
return browser.New(conf.Browser.Port)
}
return nil, fmt.Errorf("unknown player: \"%s\"", conf.Player)
// Split by comma to support multi-player configuration while keeping single value compatible
raw := strings.Split(conf.Player, ",")
// If no comma present, raw will contain the original single value

var errs []string
for _, name := range raw {
n := strings.TrimSpace(name)
if n == "" {
continue
}

switch n {
case "spotify":
p, err := spotify.New()
if err != nil {
errs = append(errs, fmt.Sprintf("spotify: %v", err))
continue
}
return p, nil
case "mpd":
p, err := mpd.New(conf.Mpd.Address, conf.Mpd.Password)
if err != nil {
errs = append(errs, fmt.Sprintf("mpd: %v", err))
continue
}
return p, nil
case "mopidy":
p, err := mopidy.New(conf.Mopidy.Address)
if err != nil {
errs = append(errs, fmt.Sprintf("mopidy: %v", err))
continue
}
return p, nil
case "mpris":
p, err := mpris.New(conf.Mpris.Players)
if err != nil {
errs = append(errs, fmt.Sprintf("mpris: %v", err))
continue
}
return p, nil
case "browser":
p, err := browser.New(conf.Browser.Port)
if err != nil {
errs = append(errs, fmt.Sprintf("browser: %v", err))
continue
}
return p, nil
default:
// Unknown player name; record and continue to allow next specified players
errs = append(errs, fmt.Sprintf("unknown player: %q", n))
continue
}
}

// If nothing succeeded, return aggregated error for easier troubleshooting
if len(errs) == 0 {
return nil, fmt.Errorf("no player specified")
}
return nil, fmt.Errorf("no player initialized successfully; tried: %s", strings.Join(errs, "; "))
}
11 changes: 9 additions & 2 deletions services/mopidy/mopidy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ import (
"net/http"
)

func New(address string) *Client {
return &Client{address: address}
func New(address string) (*Client, error) {
// Return a client without probing; but perform a lightweight health check to catch obvious misconfig
c := &Client{address: address}
// Try a simple RPC that should always be available; ignore body decoding errors, we just need connectivity
var out stateResponse
if err := c.get("core.playback.get_state", &out); err != nil {
return nil, err
}
return c, nil
}

// Client implements player.Player
Expand Down
9 changes: 7 additions & 2 deletions services/mpd/mpd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import (
"github.com/fhs/gompd/mpd"
)

func New(address, password string) *Client {
return &Client{
func New(address, password string) (*Client, error) {
c := &Client{
address: address,
password: password,
}
// Attempt initial connection so callers can decide to skip on error
if err := c.connect(); err != nil {
return nil, err
}
return c, nil
}

// Client implements player.Player
Expand Down