112 lines
4.0 KiB
Go
112 lines
4.0 KiB
Go
// Package soundcloud implements a playlist.Provider backed by yt-dlp.
|
|
//
|
|
// SoundCloud is opt-in: it only registers when [soundcloud] enabled = true
|
|
// is set in config. Once enabled, search uses yt-dlp's "scsearch:" protocol
|
|
// and works without further configuration. When [soundcloud] user is set,
|
|
// browse exposes that profile's Tracks, Likes, and Reposts — public for most
|
|
// accounts. With cookies_from set, yt-dlp picks up the user's browser session
|
|
// for subscriber-gated content.
|
|
package soundcloud
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/bjarneo/cliamp/playlist"
|
|
"github.com/bjarneo/cliamp/provider"
|
|
"github.com/bjarneo/cliamp/resolve"
|
|
)
|
|
|
|
// Compile-time interface checks.
|
|
var (
|
|
_ playlist.Provider = (*Provider)(nil)
|
|
_ provider.Searcher = (*Provider)(nil)
|
|
)
|
|
|
|
// Config holds settings for the SoundCloud provider.
|
|
type Config struct {
|
|
Enabled bool // true only when user explicitly sets enabled = true
|
|
User string // SoundCloud username (the path segment, e.g. "yourname"). Optional.
|
|
CookiesFrom string // browser name for yt-dlp --cookies-from-browser (e.g. "firefox"). Optional.
|
|
}
|
|
|
|
// IsSet reports whether the SoundCloud provider should be exposed.
|
|
// SoundCloud is opt-in: requires enabled = true in [soundcloud].
|
|
func (c Config) IsSet() bool { return c.Enabled }
|
|
|
|
// Provider implements playlist.Provider and provider.Searcher for SoundCloud
|
|
// via yt-dlp.
|
|
type Provider struct {
|
|
user string // optional configured username for browse
|
|
}
|
|
|
|
// defaultBrowse seeds the empty-user discovery view. SoundCloud's official
|
|
// charts/discover endpoints all 404 through yt-dlp, so these are search-backed
|
|
// virtual playlists — the ID is the yt-dlp page URL passed straight to
|
|
// ResolveYTDLBatch.
|
|
var defaultBrowse = []playlist.PlaylistInfo{
|
|
{ID: "scsearch50:trending", Name: "Trending", Section: "Browse"},
|
|
{ID: "scsearch50:hip hop", Name: "Hip-Hop", Section: "Browse"},
|
|
{ID: "scsearch50:electronic", Name: "Electronic", Section: "Browse"},
|
|
{ID: "scsearch50:house", Name: "House", Section: "Browse"},
|
|
{ID: "scsearch50:lo-fi", Name: "Lo-Fi", Section: "Browse"},
|
|
{ID: "scsearch50:indie", Name: "Indie", Section: "Browse"},
|
|
{ID: "scsearch50:pop", Name: "Pop", Section: "Browse"},
|
|
}
|
|
|
|
// NewFromConfig returns a provider, or nil when SoundCloud is not enabled.
|
|
// Sets resolve's yt-dlp cookies as a side effect when CookiesFrom is non-empty
|
|
// so any yt-dlp invocation (search, browse, playback) uses the user's
|
|
// signed-in session.
|
|
func NewFromConfig(cfg Config) *Provider {
|
|
if !cfg.Enabled {
|
|
return nil
|
|
}
|
|
if cfg.CookiesFrom != "" {
|
|
resolve.SetYTDLCookiesFrom(cfg.CookiesFrom)
|
|
}
|
|
return &Provider{user: strings.TrimSpace(cfg.User)}
|
|
}
|
|
|
|
func (p *Provider) Name() string { return "SoundCloud" }
|
|
|
|
// Playlists exposes the configured user's Tracks, Likes, and Reposts when a
|
|
// username is set; otherwise a curated set of genre searches so the empty
|
|
// state has something playable. Ctrl+F always opens search regardless.
|
|
func (p *Provider) Playlists() ([]playlist.PlaylistInfo, error) {
|
|
if p.user == "" {
|
|
return slices.Clone(defaultBrowse), nil
|
|
}
|
|
base := "https://soundcloud.com/" + url.PathEscape(p.user)
|
|
return []playlist.PlaylistInfo{
|
|
{ID: base + "/tracks", Name: "Tracks", Section: p.user},
|
|
{ID: base + "/likes", Name: "Likes", Section: p.user},
|
|
{ID: base + "/reposts", Name: "Reposts", Section: p.user},
|
|
}, nil
|
|
}
|
|
|
|
// Tracks resolves a SoundCloud page URL (or scsearch query) via yt-dlp. The
|
|
// playlistID is the value produced by Playlists.
|
|
func (p *Provider) Tracks(playlistID string) ([]playlist.Track, error) {
|
|
if playlistID == "" {
|
|
return nil, fmt.Errorf("soundcloud: empty playlist id")
|
|
}
|
|
return resolve.ResolveYTDLBatch(playlistID, 0, 0)
|
|
}
|
|
|
|
// SearchTracks runs `yt-dlp scsearch{limit}:{query}` and returns matched
|
|
// tracks. Implements provider.Searcher.
|
|
func (p *Provider) SearchTracks(_ context.Context, query string, limit int) ([]playlist.Track, error) {
|
|
q := strings.TrimSpace(query)
|
|
if q == "" {
|
|
return nil, nil
|
|
}
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
return resolve.ResolveYTDLBatch(fmt.Sprintf("scsearch%d:%s", limit, q), 0, 0)
|
|
}
|