Enable Spotify support on Windows (#323)
go-librespot gained first-party WASAPI output for Windows in v0.9.0 (cliamp was pinned to v0.7.1, which doesn't compile on Windows). Bump the dependency and remove the Windows CGO stub so the real provider builds on all platforms. CI now installs a MinGW toolchain via MSYS2 and builds/tests with CGO_ENABLED=1 on windows-2025, including a workaround for an MSYS2 libogg packaging issue where libogg-0.dll's export table is missing ogg_stream_iovecin even though it's present in the static libogg.a. Verified locally end-to-end on Windows: native CGO build, full test suite, and real Spotify Premium playback. Fixes #299 Not in scope for this PR: release.yml still builds Windows with CGO_ENABLED=0, so Releases binaries won't include Spotify until that pipeline is updated separately (needs a packaging decision: bundle the MSYS2 DLLs or pursue a fully static build).
This commit is contained in:
@@ -43,5 +43,32 @@ jobs:
|
||||
- name: Install codec libs (macOS)
|
||||
if: matrix.goos == 'darwin'
|
||||
run: brew install flac libvorbis libogg pkg-config
|
||||
- run: go test -count=1 ./...
|
||||
- run: go build -trimpath ./...
|
||||
- name: Set up MinGW toolchain (Windows)
|
||||
if: matrix.goos == 'windows'
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: MINGW64
|
||||
update: true
|
||||
install: >-
|
||||
mingw-w64-x86_64-gcc
|
||||
mingw-w64-x86_64-pkg-config
|
||||
mingw-w64-x86_64-libogg
|
||||
mingw-w64-x86_64-libvorbis
|
||||
mingw-w64-x86_64-flac
|
||||
mingw-w64-x86_64-mpg123
|
||||
- name: Test and build (Windows)
|
||||
if: matrix.goos == 'windows'
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
# Some MSYS2 libogg builds ship a libogg-0.dll whose export table is
|
||||
# missing ogg_stream_iovecin, even though it's present in the static
|
||||
# libogg.a. Force just that library to link statically.
|
||||
CGO_LDFLAGS: "-Wl,-Bstatic -logg -Wl,-Bdynamic"
|
||||
run: |
|
||||
go test -count=1 ./...
|
||||
go build -trimpath ./...
|
||||
- name: Test and build (non-Windows)
|
||||
if: matrix.goos != 'windows'
|
||||
run: |
|
||||
go test -count=1 ./...
|
||||
go build -trimpath ./...
|
||||
|
||||
@@ -61,8 +61,7 @@ Download from [GitHub Releases](https://github.com/bjarneo/cliamp/releases/lates
|
||||
> sound server — see [Troubleshooting](#troubleshooting).
|
||||
>
|
||||
> **Windows:** download `cliamp-windows-amd64.exe` from Releases. If `HOME` is not
|
||||
> set, cliamp stores its config under `%APPDATA%\cliamp`. The Spotify provider is
|
||||
> currently unavailable on Windows builds.
|
||||
> set, cliamp stores its config under `%APPDATA%\cliamp`.
|
||||
|
||||
**Optional runtime dependencies** (all platforms, all install methods):
|
||||
|
||||
@@ -132,7 +131,26 @@ sudo pacman -S alsa-lib
|
||||
|
||||
**macOS:** No extra dependencies — CoreAudio is used.
|
||||
|
||||
**Windows:** No extra SDKs required for the core player. `ffmpeg.exe` and `yt-dlp.exe` remain optional runtime dependencies for the same formats/providers as on other platforms. Spotify is not available on Windows builds.
|
||||
**Windows:** No extra SDKs required for the core player — it uses pure-Go audio decoding. `ffmpeg.exe` and `yt-dlp.exe` remain optional runtime dependencies for the same formats/providers as on other platforms.
|
||||
|
||||
Spotify support uses `go-librespot`, which needs CGO and a MinGW toolchain:
|
||||
|
||||
1. Install [MSYS2](https://www.msys2.org/).
|
||||
2. Open the **MSYS2 MinGW64** terminal (not the plain MSYS2 terminal) and install the toolchain and codec libraries:
|
||||
```sh
|
||||
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkg-config \
|
||||
mingw-w64-x86_64-libogg mingw-w64-x86_64-libvorbis \
|
||||
mingw-w64-x86_64-flac mingw-w64-x86_64-mpg123
|
||||
```
|
||||
3. From that same MinGW64 terminal (so `gcc`/`pkg-config` are on `PATH`), build with CGO enabled:
|
||||
```sh
|
||||
CGO_ENABLED=1 go build -o cliamp.exe .
|
||||
```
|
||||
Some MSYS2 `libogg` builds ship a `libogg-0.dll` whose export table is missing `ogg_stream_iovecin`, even though it's present in the static `libogg.a`. If the link fails with `undefined reference to 'ogg_stream_iovecin'`, force static linking of just that library:
|
||||
```sh
|
||||
CGO_LDFLAGS="-Wl,-Bstatic -logg -Wl,-Bdynamic" CGO_ENABLED=1 go build -o cliamp.exe .
|
||||
```
|
||||
4. `cliamp.exe` dynamically links `libvorbis`, `libvorbisenc`, `libvorbisfile`, `libFLAC`, and `libmpg123` from MSYS2. Either keep `C:\msys64\mingw64\bin` on `PATH` at runtime, or copy `libvorbis-0.dll`, `libvorbisenc-2.dll`, `libvorbisfile-3.dll`, `libFLAC.dll`, and `libmpg123-0.dll` next to `cliamp.exe`.
|
||||
|
||||
**Clone and build:**
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
Cliamp can stream your [Spotify](https://www.spotify.com/) library directly through its audio pipeline. EQ, visualizer, and all effects apply. Requires a [Spotify Premium](https://www.spotify.com/premium/) account.
|
||||
|
||||
> **Windows:** Spotify is currently unavailable on Windows builds because the `go-librespot` playback backend used by cliamp does not compile there yet.
|
||||
> **Windows:** Spotify support requires building cliamp with CGO enabled and a MinGW toolchain — see [Building from source](../README.md#building-from-source) in the README. Pre-built Windows binaries from Releases include Spotify support.
|
||||
>
|
||||
> **Quick start:** run `cliamp setup`, pick Spotify, and follow the prompts. The recommended path is to register your own Spotify Developer app and paste its `client_id` for a private Web API rate-limit quota. Cliamp authorizes playback separately with Spotify's built-in identity. A built-in shared `client_id` is also available for users who specifically need Spotify search.
|
||||
|
||||
|
||||
Vendored
-2
@@ -1,5 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
package spotify
|
||||
|
||||
import (
|
||||
|
||||
Vendored
-2
@@ -1,5 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
package spotify
|
||||
|
||||
import (
|
||||
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
package spotify
|
||||
|
||||
import (
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
package spotify
|
||||
|
||||
import (
|
||||
|
||||
Vendored
-2
@@ -1,5 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
package spotify
|
||||
|
||||
import (
|
||||
|
||||
Vendored
+5
-4
@@ -1,5 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
package spotify
|
||||
|
||||
import (
|
||||
@@ -10,6 +8,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
@@ -292,8 +291,10 @@ func TestWebAPITokenSourcePersistsRotatedRefreshToken(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mode := info.Mode().Perm(); mode != 0o600 {
|
||||
t.Errorf("credentials mode = %o, want 600", mode)
|
||||
if runtime.GOOS != "windows" {
|
||||
if mode := info.Mode().Perm(); mode != 0o600 {
|
||||
t.Errorf("credentials mode = %o, want 600", mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
-2
@@ -1,5 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package spotify integrates Spotify playback into cliamp via go-librespot.
|
||||
package spotify
|
||||
|
||||
|
||||
Vendored
-2
@@ -1,5 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
package spotify
|
||||
|
||||
import (
|
||||
|
||||
Vendored
-71
@@ -1,71 +0,0 @@
|
||||
//go:build windows
|
||||
|
||||
// stub_windows.go provides a no-op Spotify implementation on Windows
|
||||
// where go-librespot (CGO: FLAC, Vorbis, ALSA) cannot compile.
|
||||
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/gopxl/beep/v2"
|
||||
|
||||
"github.com/bjarneo/cliamp/playlist"
|
||||
)
|
||||
|
||||
var errSpotifyUnavailable = errors.New("spotify: unavailable on Windows (go-librespot requires CGO)")
|
||||
|
||||
// Session is a no-op on Windows.
|
||||
type Session struct{}
|
||||
|
||||
// SpotifyProvider is a no-op on Windows.
|
||||
type SpotifyProvider struct{}
|
||||
|
||||
// New returns nil — Spotify is disabled on Windows because
|
||||
// go-librespot requires CGO (FLAC, Vorbis, ALSA) which cannot
|
||||
// cross-compile. Callers must nil-check the return value.
|
||||
// bitrate is ignored on this platform.
|
||||
func New(_ *Session, _ string, _ int) *SpotifyProvider { return nil }
|
||||
|
||||
// Close is a no-op.
|
||||
func (p *SpotifyProvider) Close() {}
|
||||
|
||||
// Name returns the provider name.
|
||||
func (p *SpotifyProvider) Name() string { return "Spotify" }
|
||||
|
||||
// Playlists returns nil — Spotify is unavailable on Windows.
|
||||
func (p *SpotifyProvider) Playlists() ([]playlist.PlaylistInfo, error) { return nil, nil }
|
||||
|
||||
// Tracks returns nil — Spotify is unavailable on Windows.
|
||||
func (p *SpotifyProvider) Tracks(_ string) ([]playlist.Track, error) { return nil, nil }
|
||||
|
||||
// Authenticate is a no-op.
|
||||
func (p *SpotifyProvider) Authenticate() error { return nil }
|
||||
|
||||
// URISchemes returns the URI prefixes handled by this provider.
|
||||
func (p *SpotifyProvider) URISchemes() []string { return []string{"spotify:"} }
|
||||
|
||||
// NewStreamer returns an error — Spotify streaming is unavailable on Windows.
|
||||
func (p *SpotifyProvider) NewStreamer(_ string) (beep.StreamSeekCloser, beep.Format, time.Duration, error) {
|
||||
return nil, beep.Format{}, 0, errSpotifyUnavailable
|
||||
}
|
||||
|
||||
// SearchTracks is a no-op on Windows.
|
||||
func (p *SpotifyProvider) SearchTracks(_ context.Context, _ string, _ int) ([]playlist.Track, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// AddTrackToPlaylist is a no-op on Windows.
|
||||
func (p *SpotifyProvider) AddTrackToPlaylist(_ context.Context, _ string, _ playlist.Track) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePlaylist is a no-op on Windows.
|
||||
func (p *SpotifyProvider) CreatePlaylist(_ context.Context, _ string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// SetAuthURLObserver is a no-op on Windows.
|
||||
func SetAuthURLObserver(_ func(string)) {}
|
||||
@@ -6,7 +6,7 @@ require (
|
||||
charm.land/bubbletea/v2 v2.0.2
|
||||
charm.land/lipgloss/v2 v2.0.2
|
||||
github.com/charmbracelet/x/ansi v0.11.6
|
||||
github.com/devgianlu/go-librespot v0.7.1
|
||||
github.com/devgianlu/go-librespot v0.9.0
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
github.com/gopxl/beep/v2 v2.1.1
|
||||
|
||||
@@ -43,8 +43,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/devgianlu/go-librespot v0.7.1 h1:d/t1OrUHXLIcxJw3zrQ6W2STrg2Jg+uneVwp36qAeAs=
|
||||
github.com/devgianlu/go-librespot v0.7.1/go.mod h1:tcLnLzCmW9xbPe2Uo0wMiu08d1wzNI2b2zuWlFYEx00=
|
||||
github.com/devgianlu/go-librespot v0.9.0 h1:XMXVDUdmry3bEVfHnWfgDtgvvCefzikrkon8aqh43sc=
|
||||
github.com/devgianlu/go-librespot v0.9.0/go.mod h1:tcLnLzCmW9xbPe2Uo0wMiu08d1wzNI2b2zuWlFYEx00=
|
||||
github.com/devgianlu/shannon v0.0.0-20230613115856-82ec90b7fa7e h1:OoETp+L//8ZDtd5BWKaogHQjgA104yF4a2yqjfaG3mE=
|
||||
github.com/devgianlu/shannon v0.0.0-20230613115856-82ec90b7fa7e/go.mod h1:m5DMFz6BcaKJwxxPaSh9MxwPzK2GPSt1KRFC8Imf0ik=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
|
||||
|
||||
@@ -105,11 +105,7 @@ func run(overrides config.Overrides, positional []string, daemon bool) error {
|
||||
if cfg.Spotify.IsSet() {
|
||||
clientID := cfg.Spotify.ResolveClientID(spotify.DefaultClientID)
|
||||
spotifyProv = spotify.New(nil, clientID, cfg.Spotify.Bitrate)
|
||||
if spotifyProv != nil {
|
||||
providers = append(providers, model.ProviderEntry{Key: "spotify", Name: "Spotify", Provider: spotifyProv})
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "Spotify is unavailable in this Windows build.")
|
||||
}
|
||||
providers = append(providers, model.ProviderEntry{Key: "spotify", Name: "Spotify", Provider: spotifyProv})
|
||||
}
|
||||
|
||||
var qobuzProv *qobuz.QobuzProvider
|
||||
|
||||
Reference in New Issue
Block a user