From 47af5c05c3c6f8e354c7504e8654e8ddf4857c9e Mon Sep 17 00:00:00 2001 From: Sergio Rubio Date: Sun, 31 May 2026 21:43:17 +0200 Subject: [PATCH 01/21] Add Qobuz to cliamp setup wizard --- cmd/setup.go | 36 +++++++++++++++++++++++++++++++++++- cmd/setup_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/cmd/setup.go b/cmd/setup.go index ef24bedcc..bb6f732ae 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -1,7 +1,7 @@ // Package cmd implements interactive subcommands invoked from the CLI. // setup.go contains the provider onboarding wizard reachable via // `cliamp setup`. It walks the user through configuring each remote -// provider (Navidrome, Plex, Jellyfin, Spotify, NetEase, YouTube Music), +// provider (Navidrome, Plex, Jellyfin, Spotify, Qobuz, NetEase, YouTube Music), // validates the connection where possible, and writes the resulting // TOML section to ~/.config/cliamp/config.toml. // @@ -92,6 +92,7 @@ const ( keyNetEaseBrowser = "_netease_browser" keyYTMusicMode = "_mode" keySpotifyMode = "_spotify_mode" + keyQobuzQuality = "_qobuz_quality" ) func providers() []providerSpec { @@ -287,6 +288,39 @@ func providers() []providerSpec { return strings.Join(lines, "\n") }, }, + { + key: "qobuz", + name: "Qobuz", + section: "qobuz", + intro: []string{ + "Lossless streaming. Requires an active Qobuz subscription.", + "", + "No API credentials needed — cliamp obtains them automatically.", + "After setup, launch cliamp, select Qobuz, and press Enter to", + "sign in via OAuth in your browser. Hi-Res tiers require a plan", + "that includes them.", + }, + picker: &pickerSpec{ + key: keyQobuzQuality, + label: "Stream quality", + options: []pickerOption{ + {value: "6", label: "FLAC 16-bit/44.1kHz (CD) — recommended"}, + {value: "7", label: "FLAC 24-bit up to 96kHz (Hi-Res)"}, + {value: "27", label: "FLAC 24-bit up to 192kHz (Hi-Res)"}, + {value: "5", label: "MP3 320kbps"}, + }, + }, + body: func(v map[string]string) string { + q := v[keyQobuzQuality] + if q == "" { + q = "6" + } + return strings.Join([]string{ + "enabled = true", + fmt.Sprintf("quality = %s", q), + }, "\n") + }, + }, { key: "netease", name: "NetEase Cloud Music", diff --git a/cmd/setup_test.go b/cmd/setup_test.go index 1b132a689..783699878 100644 --- a/cmd/setup_test.go +++ b/cmd/setup_test.go @@ -248,6 +248,37 @@ func TestNetEaseSetupBody(t *testing.T) { } } +func TestQobuzSetupBody(t *testing.T) { + spec := providerSpec{} + for _, p := range providers() { + if p.section == "qobuz" { + spec = p + break + } + } + if spec.section == "" { + t.Fatal("qobuz spec missing") + } + + // Explicit quality selection. + body := spec.body(map[string]string{keyQobuzQuality: "27"}) + for _, want := range []string{"enabled = true", "quality = 27"} { + if !strings.Contains(body, want) { + t.Fatalf("body missing %q: %q", want, body) + } + } + + // Default quality when none picked. + if got := spec.body(map[string]string{}); !strings.Contains(got, "quality = 6") { + t.Fatalf("default quality not 6: %q", got) + } + + // No live probe (auth happens interactively in the TUI). + if spec.validate != nil { + t.Fatal("qobuz spec should not define a validate probe") + } +} + func TestNetEasePickerSelectionFiltersFields(t *testing.T) { base := newSetupModel() neteaseIdx := -1 From e71b086b80d8db80269c1a29e6673dc8b7aad0c1 Mon Sep 17 00:00:00 2001 From: Sergio Rubio Date: Sun, 31 May 2026 21:45:20 +0200 Subject: [PATCH 02/21] hook up qobuz cli command --- commands.go | 40 ++++++++++++++++++++++++++++++++++++---- docs/cli.md | 2 +- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/commands.go b/commands.go index 9484290be..842d09c14 100644 --- a/commands.go +++ b/commands.go @@ -14,6 +14,7 @@ import ( "cliamp/applog" "cliamp/cmd" "cliamp/config" + "cliamp/external/qobuz" "cliamp/external/spotify" "cliamp/ipc" "cliamp/player" @@ -32,7 +33,7 @@ func buildApp() *cli.Command { &cli.BoolFlag{Name: "no-mono", Usage: "disable mono output"}, &cli.BoolFlag{Name: "auto-play", Usage: "start playback immediately"}, &cli.BoolFlag{Name: "compact", Usage: "compact mode (80 columns)"}, - &cli.StringFlag{Name: "provider", Usage: "default provider: radio, navidrome, plex, jellyfin, emby, spotify, soundcloud, netease, yt, youtube, ytmusic"}, + &cli.StringFlag{Name: "provider", Usage: "default provider: radio, navidrome, plex, jellyfin, emby, spotify, qobuz, soundcloud, netease, yt, youtube, ytmusic"}, &cli.StringFlag{Name: "start-theme", Usage: "UI theme name"}, &cli.StringFlag{Name: "visualizer", Usage: "visualizer mode"}, &cli.StringFlag{Name: "eq-preset", Usage: "EQ preset name"}, @@ -69,6 +70,7 @@ func buildApp() *cli.Command { historyCommand(), setupCommand(), spotifyCommand(), + qobuzCommand(), ipcSimpleCommand("play", "resume playback"), ipcSimpleCommand("pause", "pause playback"), ipcSimpleCommand("toggle", "play/pause toggle"), @@ -150,10 +152,10 @@ func overridesFromFlags(c *cli.Command) (config.Overrides, error) { if c.IsSet("provider") { v := strings.ToLower(c.String("provider")) switch v { - case "radio", "navidrome", "spotify", "plex", "jellyfin", "emby", "soundcloud", "netease", "yt", "youtube", "ytmusic": + case "radio", "navidrome", "spotify", "qobuz", "plex", "jellyfin", "emby", "soundcloud", "netease", "yt", "youtube", "ytmusic": ov.Provider = &v default: - return ov, fmt.Errorf("--provider must be radio, navidrome, spotify, plex, jellyfin, emby, soundcloud, netease, yt, youtube, or ytmusic (got %q)", v) + return ov, fmt.Errorf("--provider must be radio, navidrome, spotify, qobuz, plex, jellyfin, emby, soundcloud, netease, yt, youtube, or ytmusic (got %q)", v) } } if c.IsSet("start-theme") { @@ -301,7 +303,7 @@ func setupCommand() *cli.Command { Name: "setup", Usage: "interactive wizard to configure remote providers", Description: "Walks through configuring Navidrome, Plex, Jellyfin, Spotify,\n" + - "NetEase, and YouTube Music. Validates connections and writes\n" + + "Qobuz, NetEase, and YouTube Music. Validates connections and writes\n" + "~/.config/cliamp/config.toml.", Action: func(ctx context.Context, c *cli.Command) error { return cmd.Setup() @@ -339,6 +341,36 @@ func spotifyCommand() *cli.Command { } } +func qobuzCommand() *cli.Command { + return &cli.Command{ + Name: "qobuz", + Usage: "manage Qobuz integration", + Commands: []*cli.Command{ + { + Name: "reset", + Usage: "clear stored Qobuz credentials and force re-authentication", + Action: func(ctx context.Context, c *cli.Command) error { + path, err := qobuz.CredsPath() + if err != nil { + return fmt.Errorf("locate credentials: %w", err) + } + removed, err := qobuz.DeleteCreds() + if err != nil { + return fmt.Errorf("remove credentials: %w", err) + } + if !removed { + fmt.Println("No stored Qobuz credentials to remove.") + return nil + } + fmt.Printf("Removed %s\n", path) + fmt.Println("Restart cliamp and select Qobuz to sign in again.") + return nil + }, + }, + }, + } +} + func playlistCommand() *cli.Command { return &cli.Command{ Name: "playlist", diff --git a/docs/cli.md b/docs/cli.md index 37df870a0..22ed88d03 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -115,7 +115,7 @@ CLI flags override config file values for the current session only. They are not ## Setup wizard -Configure remote providers (Navidrome, Plex, Jellyfin, Emby, Spotify, NetEase, YouTube Music) through a small TUI. Each provider page links to where to find the required credentials, validates the connection live, and writes the resulting `[provider]` block to `~/.config/cliamp/config.toml` without disturbing the rest of the file. +Configure remote providers (Navidrome, Plex, Jellyfin, Emby, Spotify, Qobuz, NetEase, YouTube Music) through a small TUI. Each provider page links to where to find the required credentials, validates the connection live, and writes the resulting `[provider]` block to `~/.config/cliamp/config.toml` without disturbing the rest of the file. ```sh cliamp setup From afcf6959b325488873ea3b81981f1ae348aebb63 Mon Sep 17 00:00:00 2001 From: Sergio Rubio Date: Sun, 31 May 2026 21:46:42 +0200 Subject: [PATCH 03/21] Add Qobuz configuration --- config.toml.example | 20 +++++++++++++++- config/config.go | 29 +++++++++++++++++++++++ config/config_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++ docs/configuration.md | 2 +- 4 files changed, 104 insertions(+), 2 deletions(-) diff --git a/config.toml.example b/config.toml.example index 76c37df85..306bc9641 100644 --- a/config.toml.example +++ b/config.toml.example @@ -43,7 +43,7 @@ eq_preset = "Flat" # Only used when eq_preset is "Custom" or empty eq = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -# Default provider on startup: "radio", "navidrome", "spotify", "plex", "jellyfin", "emby", "soundcloud", "netease", or a YouTube provider +# Default provider on startup: "radio", "navidrome", "spotify", "qobuz", "plex", "jellyfin", "emby", "soundcloud", "netease", or a YouTube provider # provider = "radio" # Compact mode: cap UI width at 80 columns (default: fluid/full-width) @@ -83,6 +83,24 @@ eq = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] # rate-limit quota is shared with every librespot-based client and you # may see occasional 429s. +# --- +# Qobuz (optional) +# Requires an active Qobuz subscription. +# +# Enable the provider, then run cliamp, select Qobuz, and press Enter to +# sign in. A browser window opens for Qobuz's OAuth login; credentials are +# cached at ~/.config/cliamp/qobuz_credentials.json and refreshed silently +# on later launches. Run "cliamp qobuz reset" to clear them. +# [qobuz] +# enabled = true +# +# Stream quality (format_id). Default 6. +# 5 = MP3 320kbps +# 6 = FLAC 16-bit/44.1kHz (CD) +# 7 = FLAC 24-bit up to 96kHz +# 27 = FLAC 24-bit up to 192kHz (Hi-Res) +# quality = 6 + # --- # Navidrome / Subsonic server (optional) # When configured, cliamp opens the playlist browser on startup and streams diff --git a/config/config.go b/config/config.go index bfecd8835..0882b420a 100644 --- a/config/config.go +++ b/config/config.go @@ -105,6 +105,22 @@ func (s SpotifyConfig) ResolveClientID(fallbackID string) string { return fallbackID } +// QobuzConfig holds settings for the Qobuz provider. Requires a paid Qobuz +// subscription (Studio/Sublime). The app_id, signing secrets and OAuth private +// key are scraped automatically from the Qobuz web player, so no developer +// credentials are needed. Sign-in is an interactive OAuth browser flow. +type QobuzConfig struct { + Disabled bool // true only when user explicitly sets enabled = false + Enabled bool // true when [qobuz] section exists + Quality int // preferred stream format_id: 5 (MP3 320), 6 (FLAC CD), 7 (Hi-Res ≤96kHz), 27 (Hi-Res ≤192kHz) +} + +// IsSet reports whether the Qobuz provider should be shown. Section presence +// is enough; credentials are scraped from the Qobuz web player automatically. +func (q QobuzConfig) IsSet() bool { + return !q.Disabled && q.Enabled +} + // YouTubeMusicConfig holds settings for the YouTube Music provider. // If no client_id/client_secret are set, built-in fallback credentials are // used automatically (same pattern as Spotify). @@ -244,6 +260,7 @@ type Config struct { InitialDirectory string // initial directory for the file browser Navidrome NavidromeConfig // optional Navidrome/Subsonic server credentials Spotify SpotifyConfig // optional Spotify provider (requires Premium) + Qobuz QobuzConfig // optional Qobuz provider (requires subscription) YouTubeMusic YouTubeMusicConfig // optional YouTube Music provider Plex PlexConfig // optional Plex Media Server credentials Jellyfin JellyfinConfig // optional Jellyfin server credentials @@ -274,6 +291,7 @@ func defaultConfig() Config { PaddingH: 3, PaddingV: 1, Spotify: SpotifyConfig{Bitrate: 320}, + Qobuz: QobuzConfig{Quality: 6}, LogLevel: "info", } } @@ -316,6 +334,8 @@ func Load() (Config, error) { section = "ytmusic" // normalize for key parsing below case "spotify": cfg.Spotify.Enabled = true + case "qobuz": + cfg.Qobuz.Enabled = true } // Initialize plugin sub-maps for [plugins] and [plugins.*] sections. if section == "plugins" || strings.HasPrefix(section, "plugins.") { @@ -366,6 +386,15 @@ func Load() (Config, error) { cfg.Spotify.Bitrate = v } } + case "qobuz": + switch key { + case "enabled": + cfg.Qobuz.Disabled = strings.ToLower(val) == "false" + case "quality": + if v, err := strconv.Atoi(val); err == nil { + cfg.Qobuz.Quality = v + } + } case "ytmusic": switch key { case "enabled": diff --git a/config/config_test.go b/config/config_test.go index 2c11a70cd..942e1d87c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -440,6 +440,61 @@ func TestLoadSpotifyBitrate(t *testing.T) { } } +func TestQobuzIsSet(t *testing.T) { + tests := []struct { + name string + cfg QobuzConfig + want bool + }{ + {"section present", QobuzConfig{Enabled: true}, true}, + {"explicitly disabled", QobuzConfig{Enabled: true, Disabled: true}, false}, + {"absent", QobuzConfig{}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.cfg.IsSet(); got != tt.want { + t.Errorf("IsSet() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLoadQobuz(t *testing.T) { + tests := []struct { + name string + body string + wantIsSet bool + wantQuality int + }{ + {"section enables, default quality", "[qobuz]\n", true, 6}, + {"explicit quality", "[qobuz]\nquality = 27\n", true, 27}, + {"disabled", "[qobuz]\nenabled = false\n", false, 6}, + {"absent", "", false, 6}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + path := filepath.Join(os.Getenv("HOME"), ".config", "cliamp", "config.toml") + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + if err := os.WriteFile(path, []byte(tt.body), 0o644); err != nil { + t.Fatalf("WriteFile: %v", err) + } + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if got := cfg.Qobuz.IsSet(); got != tt.wantIsSet { + t.Errorf("Qobuz.IsSet() = %v, want %v", got, tt.wantIsSet) + } + if cfg.Qobuz.Quality != tt.wantQuality { + t.Errorf("Qobuz.Quality = %d, want %d", cfg.Qobuz.Quality, tt.wantQuality) + } + }) + } +} + func TestPlexIsSet(t *testing.T) { tests := []struct { name string diff --git a/docs/configuration.md b/docs/configuration.md index 5bb895994..3eb21b538 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,6 @@ # Configuration -For remote providers (Navidrome, Plex, Jellyfin, Emby, Spotify, NetEase, YouTube Music), the fastest path is the interactive wizard: +For remote providers (Navidrome, Plex, Jellyfin, Emby, Spotify, Qobuz, NetEase, YouTube Music), the fastest path is the interactive wizard: ```sh cliamp setup From 08ede29e17228581dd9cdb55da33899eba69ceff Mon Sep 17 00:00:00 2001 From: Sergio Rubio Date: Sun, 31 May 2026 21:48:40 +0200 Subject: [PATCH 04/21] map qobuz keybindings --- docs/keybindings.md | 1 + ui/model/keymap.go | 3 ++- ui/model/keys.go | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/keybindings.md b/docs/keybindings.md index b1e830226..d512f39a9 100644 --- a/docs/keybindings.md +++ b/docs/keybindings.md @@ -65,6 +65,7 @@ Press `?` or `Ctrl+K` in the player to see all keybindings. | `Y` | Open YouTube provider | | `C` | Open SoundCloud provider | | `M` | Open NetEase provider | +| `Q` | Open Qobuz provider | ## Playlist and Queue diff --git a/ui/model/keymap.go b/ui/model/keymap.go index f627a7480..4f908ff90 100644 --- a/ui/model/keymap.go +++ b/ui/model/keymap.go @@ -53,6 +53,7 @@ var keymapEntries = []keymapEntry{ {key: "M", action: "Open NetEase provider"}, {key: "J", action: "Open Jellyfin provider"}, {key: "E", action: "Open Emby provider"}, + {key: "Q", action: "Open Qobuz provider"}, {key: "Ctrl+J", action: "Jump to time"}, {key: "p", action: "Playlist manager"}, {key: "Ctrl+H", action: "Toggle album headers"}, @@ -103,7 +104,7 @@ var coreReservedKeys = []string{ "r", "z", "m", "e", "a", "A", "ctrl+h", "ctrl+s", "S", "/", "ctrl+f", "ctrl+j", "J", "E", "p", "t", "i", "y", "o", "u", - "N", "L", "R", "P", "Y", "C", "M", + "N", "L", "R", "P", "Y", "C", "M", "Q", "v", "V", "ctrl+v", "ctrl+x", "x", "d", "ctrl+k", "?", "ctrl+r", } diff --git a/ui/model/keys.go b/ui/model/keys.go index 35efc8d43..ad38c4bcc 100644 --- a/ui/model/keys.go +++ b/ui/model/keys.go @@ -367,6 +367,8 @@ func (m *Model) handleKey(msg tea.KeyPressMsg) tea.Cmd { return m.switchToProvider("soundcloud") case "M": return m.switchToProvider("netease") + case "Q": + return m.switchToProvider("qobuz") case "L": return m.switchToProvider("local") case "R": @@ -746,6 +748,8 @@ func (m *Model) handleKey(msg tea.KeyPressMsg) tea.Cmd { return m.switchToProvider("soundcloud") case "M": return m.switchToProvider("netease") + case "Q": + return m.switchToProvider("qobuz") case "ctrl+h": m.toggleAlbumHeadersManual() From 3761c5ae5ded1b53f4ae2cc2fdd84ca3a032b2a3 Mon Sep 17 00:00:00 2001 From: Sergio Rubio Date: Sun, 31 May 2026 21:50:35 +0200 Subject: [PATCH 05/21] qobuz integration documentation --- docs/qobuz.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/qobuz.md diff --git a/docs/qobuz.md b/docs/qobuz.md new file mode 100644 index 000000000..0370a2b49 --- /dev/null +++ b/docs/qobuz.md @@ -0,0 +1,85 @@ +# Qobuz Integration + +cliamp can stream your [Qobuz](https://www.qobuz.com/) library directly through its audio pipeline. EQ, visualizer, and all effects apply. Requires an active Qobuz subscription. + +Qobuz delivers lossless FLAC, so cliamp streams it through the same buffer-while-playing + ffmpeg pipeline used for other lossless providers. `ffmpeg` must be on `PATH`. + +## Setup + +The fastest path is the interactive wizard — run `cliamp setup`, pick **Qobuz**, choose a stream quality, and it writes the `[qobuz]` block for you. + +Or configure it manually in `~/.config/cliamp/config.toml`: + +```toml +[qobuz] +enabled = true +quality = 6 +``` + +No developer credentials are needed — the `app_id`, signing secrets, and OAuth private key are scraped automatically from the Qobuz web player. + +Run `cliamp`, select Qobuz as a provider, and press `Enter` to sign in. A browser window opens for Qobuz's OAuth login. Once you authorize, credentials are cached at `~/.config/cliamp/qobuz_credentials.json` and subsequent launches refresh silently. + +> **Click "Back" to finish.** After you authorize, Qobuz shows a *"You are signed in, you can leave this page"* screen with a **Back** button (e.g. "Atrás") rather than redirecting automatically. Click that **Back** button — it fires the redirect that hands the sign-in code to cliamp and completes authentication. cliamp waits (up to 5 minutes) for it. + +### Quality + +`quality` selects the Qobuz `format_id`. If omitted, cliamp uses `6` (FLAC CD). Supported values: + +| Value | Format | +|---|---| +| `5` | MP3 320 kbps | +| `6` | FLAC 16-bit / 44.1 kHz (CD) | +| `7` | FLAC 24-bit up to 96 kHz (Hi-Res) | +| `27` | FLAC 24-bit up to 192 kHz (Hi-Res) | + +Hi-Res tiers require a Qobuz plan that includes them. Any other value falls back to `6`. + +## Usage + +Start directly on Qobuz: + +```sh +cliamp --provider qobuz +``` + +Once authenticated, Qobuz appears as a provider alongside the others. Press `Q` to jump straight to Qobuz, or `Esc`/`b` to open the provider browser and select it. + +The provider surfaces your Qobuz library: + +- **Favorite Tracks** — your liked songs. +- **Your playlists** — playlists you created or subscribed to. +- **Favorite albums** — browsable in the album view. +- **Favorite artists** — browse an artist to see their albums. + +Press `Ctrl+F` while Qobuz is active to search the Qobuz catalog for tracks. + +## Controls + +When focused on the provider panel: + +| Key | Action | +|---|---| +| `Up` `Down` / `j` `k` | Navigate | +| `Enter` | Load the selected playlist/album or play the selected track | +| `Ctrl+F` | Search Qobuz tracks | +| `Ctrl+R` | Refresh (re-resolves stream URLs) | +| `Tab` | Switch between provider and playlist focus | +| `Esc` / `b` | Open provider browser | + +After loading a playlist or album you return to the standard playlist view with all the usual controls (seek, volume, EQ, shuffle, repeat, queue, search, lyrics). + +## Troubleshooting + +- **"OAuth failed" / browser doesn't open**: cliamp opens a localhost redirect listener on a random port. Make sure nothing is blocking outbound access to `qobuz.com` and that a default browser is configured. The flow times out after 5 minutes. +- **Sign-in seems to hang / "you can leave this page"**: after authorizing, the Qobuz OAuth page shows a confirmation screen with a **Back** button ("Atrás") instead of redirecting automatically. Click **Back** to complete sign-in. cliamp keeps waiting (up to 5 minutes) until the redirect arrives. +- **Re-authenticate**: run `cliamp qobuz reset` to clear stored credentials, then relaunch cliamp and select Qobuz to sign in again. (Equivalent to deleting `~/.config/cliamp/qobuz_credentials.json` manually.) +- **Track is unplayable / skipped**: the track may not be streamable on your subscription tier or in your region. cliamp marks such tracks unplayable and moves on. +- **Hi-Res not delivered**: setting `quality = 27` does not upgrade a tier that lacks Hi-Res. Qobuz returns the best your plan allows. +- **Stalls after a long idle session**: signed stream URLs expire over time. Press `Ctrl+R` to refresh, which re-resolves the URLs. + +## Requirements + +- An active Qobuz subscription +- `ffmpeg` on `PATH` for FLAC decoding +- No developer/API registration — credentials are obtained automatically From 8fe484f1193e81b06ec28ae1a8540cbdb1088864 Mon Sep 17 00:00:00 2001 From: Sergio Rubio Date: Sun, 31 May 2026 21:50:53 +0200 Subject: [PATCH 06/21] Register qobuz integration --- main.go | 18 +++++++++++++++++- provider/types.go | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 12baeab61..dc8849905 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "cliamp/external/navidrome" "cliamp/external/netease" "cliamp/external/plex" + "cliamp/external/qobuz" "cliamp/external/radio" "cliamp/external/radiometa" "cliamp/external/soundcloud" @@ -105,6 +106,12 @@ func run(overrides config.Overrides, positional []string, daemon bool) error { } } + var qobuzProv *qobuz.QobuzProvider + if cfg.Qobuz.IsSet() { + qobuzProv = qobuz.New(cfg.Qobuz.Quality) + providers = append(providers, model.ProviderEntry{Key: "qobuz", Name: "Qobuz", Provider: qobuzProv}) + } + if scProv := soundcloud.NewFromConfig(soundcloud.Config{ Enabled: cfg.SoundCloud.Enabled, User: cfg.SoundCloud.User, @@ -173,6 +180,9 @@ func run(overrides config.Overrides, positional []string, daemon bool) error { if spotifyProv != nil { defer spotifyProv.Close() } + if qobuzProv != nil { + defer qobuzProv.Close() + } if ytProviders.Music != nil { defer ytProviders.Music.Close() } @@ -259,7 +269,7 @@ func run(overrides config.Overrides, positional []string, daemon bool) error { } p.RegisterBufferedURLMatcher(func(u string) bool { - return navidrome.IsSubsonicStreamURL(u) || jellyfin.IsStreamURL(u) || emby.IsStreamURL(u) || plex.IsStreamURL(u) + return navidrome.IsSubsonicStreamURL(u) || jellyfin.IsStreamURL(u) || emby.IsStreamURL(u) || plex.IsStreamURL(u) || qobuz.IsStreamURL(u) }) // Pull now-playing for stations that carry no inline ICY metadata (NTS, FIP). @@ -384,6 +394,12 @@ func run(overrides config.Overrides, positional []string, daemon bool) error { }) defer spotify.SetAuthURLObserver(nil) } + if qobuzProv != nil { + qobuz.SetAuthURLObserver(func(u string) { + prog.Send(model.ProvAuthURLMsg{URL: u}) + }) + defer qobuz.SetAuthURLObserver(nil) + } svc, svcErr := wireMediaCtl(prog) if svcErr != nil { diff --git a/provider/types.go b/provider/types.go index 6a386bbe0..24f779aa5 100644 --- a/provider/types.go +++ b/provider/types.go @@ -35,4 +35,5 @@ const ( MetaJellyfinID = "jellyfin.id" MetaEmbyID = "emby.id" MetaNetEaseID = "netease.id" + MetaQobuzID = "qobuz.id" ) From b62960b53866b52f1922b4bb27cb1a320648efb0 Mon Sep 17 00:00:00 2001 From: Sergio Rubio Date: Sun, 31 May 2026 21:51:06 +0200 Subject: [PATCH 07/21] Update cliamp website index.html --- site/index.html | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/site/index.html b/site/index.html index 6acd46e80..5a96b5aaa 100644 --- a/site/index.html +++ b/site/index.html @@ -4,20 +4,20 @@ CLIAMP — Terminal Music Player - + - + - +