Files
wehub-resource-sync ead81af521
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

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)
}