feat: register the audiobookshelf provider
This commit is contained in:
@@ -25,6 +25,7 @@ import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/bjarneo/cliamp/external/audiobookshelf"
|
||||
"github.com/bjarneo/cliamp/external/emby"
|
||||
"github.com/bjarneo/cliamp/external/jellyfin"
|
||||
"github.com/bjarneo/cliamp/external/navidrome"
|
||||
@@ -89,6 +90,7 @@ type pickerOption struct {
|
||||
const (
|
||||
keyJellyfinAuth = "_auth"
|
||||
keyEmbyAuth = "_emby_auth"
|
||||
keyABSAuth = "_abs_auth"
|
||||
keyNetEaseBrowser = "_netease_browser"
|
||||
keyYTMusicMode = "_mode"
|
||||
keySpotifyMode = "_spotify_mode"
|
||||
@@ -233,6 +235,48 @@ func providers() []providerSpec {
|
||||
return strings.Join(lines, "\n")
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "audiobookshelf",
|
||||
name: "Audiobookshelf",
|
||||
section: "audiobookshelf",
|
||||
intro: []string{
|
||||
"Audiobooks and podcasts from your own Audiobookshelf server.",
|
||||
"Authenticate with an API key (Settings → Users → API Keys)",
|
||||
"or with your username and password.",
|
||||
},
|
||||
picker: &pickerSpec{
|
||||
key: keyABSAuth,
|
||||
label: "Authentication",
|
||||
options: []pickerOption{
|
||||
{value: "token", label: "API key"},
|
||||
{value: "password", label: "Username + password"},
|
||||
},
|
||||
},
|
||||
fields: []fieldSpec{
|
||||
{key: "url", label: "Server URL", help: "e.g. https://abs.example.com", required: true},
|
||||
{key: "token", label: "API key", required: true, secret: true,
|
||||
onlyIf: func(v map[string]string) bool { return v[keyABSAuth] == "token" }},
|
||||
{key: "user", label: "Username", required: true,
|
||||
onlyIf: func(v map[string]string) bool { return v[keyABSAuth] == "password" }},
|
||||
{key: "password", label: "Password", required: true, secret: true,
|
||||
onlyIf: func(v map[string]string) bool { return v[keyABSAuth] == "password" }},
|
||||
},
|
||||
validate: func(v map[string]string) error {
|
||||
return audiobookshelf.NewClient(v["url"], v["token"], v["user"], v["password"], nil).Ping()
|
||||
},
|
||||
body: func(v map[string]string) string {
|
||||
lines := []string{fmt.Sprintf("url = %q", v["url"])}
|
||||
if v[keyABSAuth] == "token" {
|
||||
lines = append(lines, fmt.Sprintf("token = %q", v["token"]))
|
||||
} else {
|
||||
lines = append(lines,
|
||||
fmt.Sprintf("user = %q", v["user"]),
|
||||
fmt.Sprintf("password = %q", v["password"]),
|
||||
)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "spotify",
|
||||
name: "Spotify (Premium)",
|
||||
|
||||
+8
-5
@@ -33,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, qobuz, soundcloud, netease, yt, youtube, ytmusic"},
|
||||
&cli.StringFlag{Name: "provider", Usage: "default provider: radio, navidrome, plex, jellyfin, emby, spotify, qobuz, soundcloud, netease, audiobookshelf, abs, 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"},
|
||||
@@ -154,11 +154,14 @@ func overridesFromFlags(c *cli.Command) (config.Overrides, error) {
|
||||
}
|
||||
if c.IsSet("provider") {
|
||||
v := strings.ToLower(c.String("provider"))
|
||||
if v == "abs" {
|
||||
v = "audiobookshelf"
|
||||
}
|
||||
switch v {
|
||||
case "radio", "navidrome", "spotify", "qobuz", "plex", "jellyfin", "emby", "soundcloud", "netease", "yt", "youtube", "ytmusic":
|
||||
case "radio", "navidrome", "spotify", "qobuz", "plex", "jellyfin", "emby", "audiobookshelf", "soundcloud", "netease", "yt", "youtube", "ytmusic":
|
||||
ov.Provider = &v
|
||||
default:
|
||||
return ov, fmt.Errorf("--provider must be radio, navidrome, spotify, qobuz, 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, audiobookshelf, soundcloud, netease, yt, youtube, or ytmusic (got %q)", v)
|
||||
}
|
||||
}
|
||||
if c.IsSet("start-theme") {
|
||||
@@ -331,8 +334,8 @@ func setupCommand() *cli.Command {
|
||||
Name: "setup",
|
||||
Usage: "interactive wizard to configure remote providers",
|
||||
Description: "Walks through configuring Navidrome, Plex, Jellyfin, Spotify,\n" +
|
||||
"Qobuz, NetEase, and YouTube Music. Validates connections and writes\n" +
|
||||
"~/.config/cliamp/config.toml.",
|
||||
"Qobuz, NetEase, Audiobookshelf, and YouTube Music. Validates connections\n" +
|
||||
"and writes ~/.config/cliamp/config.toml.",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
return cmd.Setup()
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/bjarneo/cliamp/applog"
|
||||
"github.com/bjarneo/cliamp/config"
|
||||
"github.com/bjarneo/cliamp/external/audiobookshelf"
|
||||
"github.com/bjarneo/cliamp/external/emby"
|
||||
"github.com/bjarneo/cliamp/external/jellyfin"
|
||||
"github.com/bjarneo/cliamp/external/local"
|
||||
@@ -95,6 +96,10 @@ func run(overrides config.Overrides, positional []string, daemon bool) error {
|
||||
providers = append(providers, model.ProviderEntry{Key: "emby", Name: "Emby", Provider: embyProv})
|
||||
}
|
||||
|
||||
if absProv := audiobookshelf.NewFromConfig(cfg.Audiobookshelf); absProv != nil {
|
||||
providers = append(providers, model.ProviderEntry{Key: "audiobookshelf", Name: "Audiobookshelf", Provider: absProv})
|
||||
}
|
||||
|
||||
var spotifyProv *spotify.SpotifyProvider
|
||||
if cfg.Spotify.IsSet() {
|
||||
clientID := cfg.Spotify.ResolveClientID(spotify.DefaultClientID)
|
||||
@@ -280,7 +285,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) || qobuz.IsStreamURL(u)
|
||||
return navidrome.IsSubsonicStreamURL(u) || jellyfin.IsStreamURL(u) || emby.IsStreamURL(u) || plex.IsStreamURL(u) || qobuz.IsStreamURL(u) || audiobookshelf.IsStreamURL(u)
|
||||
})
|
||||
|
||||
// Pull now-playing for stations that carry no inline ICY metadata (NTS, FIP).
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package model
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestProviderKeyForShortcutB(t *testing.T) {
|
||||
if got := providerKeyForShortcut("B"); got != "audiobookshelf" {
|
||||
t.Fatalf("providerKeyForShortcut(\"B\") = %q, want audiobookshelf", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAudiobookshelfEmptyStateHint(t *testing.T) {
|
||||
hint, ok := providerEmptyStateHint["audiobookshelf"]
|
||||
if !ok {
|
||||
t.Fatal("providerEmptyStateHint has no audiobookshelf entry")
|
||||
}
|
||||
if hint == "" {
|
||||
t.Fatal("audiobookshelf hint is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAudiobookshelfCommandRegistryEntry(t *testing.T) {
|
||||
for _, c := range commandRegistry {
|
||||
if c.Mode == commandModeMain && len(c.Keys) == 1 && c.Keys[0] == "B" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("commandRegistry has no main-mode B entry")
|
||||
}
|
||||
@@ -109,6 +109,7 @@ var commandRegistry = []commandSpec{
|
||||
{Mode: commandModeMain, Keys: []string{"M"}, KeyLabel: "M", Label: "Open NetEase provider", Keymap: true},
|
||||
{Mode: commandModeMain, Keys: []string{"J"}, KeyLabel: "J", Label: "Open Jellyfin provider", Keymap: true},
|
||||
{Mode: commandModeMain, Keys: []string{"E"}, KeyLabel: "E", Label: "Open Emby provider", Keymap: true},
|
||||
{Mode: commandModeMain, Keys: []string{"B"}, KeyLabel: "B", Label: "Open Audiobookshelf provider", Keymap: true},
|
||||
{Mode: commandModeMain, Keys: []string{"Q"}, KeyLabel: "Q", Label: "Open Qobuz provider", Keymap: true},
|
||||
{Mode: commandModeMain, Keys: []string{"ctrl+j"}, KeyLabel: "Ctrl+J", Label: "Jump to time", Keymap: true},
|
||||
{Mode: commandModeMain, Keys: []string{"p"}, KeyLabel: "p", Label: "Playlist manager", Keymap: true},
|
||||
|
||||
@@ -394,6 +394,8 @@ func (m *Model) handleKey(msg tea.KeyPressMsg) tea.Cmd {
|
||||
return m.switchToProvider("jellyfin")
|
||||
case "E":
|
||||
return m.switchToProvider("emby")
|
||||
case "B":
|
||||
return m.switchToProvider("audiobookshelf")
|
||||
case "S":
|
||||
return m.switchToProvider("spotify")
|
||||
case "P":
|
||||
@@ -731,6 +733,8 @@ func (m *Model) handleKey(msg tea.KeyPressMsg) tea.Cmd {
|
||||
return m.switchToProvider("jellyfin")
|
||||
case "E":
|
||||
return m.switchToProvider("emby")
|
||||
case "B":
|
||||
return m.switchToProvider("audiobookshelf")
|
||||
case "p":
|
||||
if m.localProvider != nil {
|
||||
m.openPlaylistManager()
|
||||
|
||||
@@ -139,6 +139,8 @@ func providerKeyForShortcut(key string) string {
|
||||
return "jellyfin"
|
||||
case "E":
|
||||
return "emby"
|
||||
case "B":
|
||||
return "audiobookshelf"
|
||||
case "Y":
|
||||
return "yt"
|
||||
case "C":
|
||||
|
||||
@@ -36,6 +36,7 @@ var providerEmptyStateHint = map[string]string{
|
||||
"navidrome": "Verify [navidrome] url/username/password in config.toml.",
|
||||
"jellyfin": "Verify [jellyfin] url and token in config.toml.",
|
||||
"emby": "Verify [emby] url and token or username/password in config.toml.",
|
||||
"audiobookshelf": "Verify [audiobookshelf] url and token or username/password in config.toml.",
|
||||
"plex": "Verify [plex] server URL and token or library filter in config.toml.",
|
||||
"youtube music": "Run `cliamp ytmusic-login` to authorize, then refresh.",
|
||||
"ytmusic": "Run `cliamp ytmusic-login` to authorize, then refresh.",
|
||||
|
||||
Reference in New Issue
Block a user