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:
Luguin
2026-08-20 12:57:01 -03:00
committed by GitHub
parent 0c9c30d1c2
commit 49c2cc7983
15 changed files with 60 additions and 103 deletions
+29 -2
View File
@@ -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 ./...
+21 -3
View File
@@ -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
View File
@@ -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.
-2
View File
@@ -1,5 +1,3 @@
//go:build !windows
package spotify
import (
-2
View File
@@ -1,5 +1,3 @@
//go:build !windows
package spotify
import (
@@ -1,5 +1,3 @@
//go:build !windows
package spotify
import (
-2
View File
@@ -1,5 +1,3 @@
//go:build !windows
package spotify
import (
-2
View File
@@ -1,5 +1,3 @@
//go:build !windows
package spotify
import (
+5 -4
View File
@@ -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)
}
}
}
-2
View File
@@ -1,5 +1,3 @@
//go:build !windows
// Package spotify integrates Spotify playback into cliamp via go-librespot.
package spotify
-2
View File
@@ -1,5 +1,3 @@
//go:build !windows
package spotify
import (
-71
View File
@@ -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)) {}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+1 -5
View File
@@ -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