Skip to content

feat: add token-efficient track search #7

Closed

Description

@rianjs

Parent: #1
Depends on: #4, #5, #6

Planned commit

feat: add token-efficient track search

Work

  • Add a thin typed Spotify client using net/http and the persistent OAuth transport; do not add a Spotify SDK.
  • Implement bounded decoding, context cancellation, Authorization-header redaction, 429 Retry-After, and at most three attempts for retryable 429/502/503/504 responses.
  • Implement sptfy search track <query>.
  • Expose -m, --max with default/ceiling 10 and --next-page-token; reject invalid input before network I/O.
  • Support --id, --fields, --extended, and --include-artwork; do not support resource JSON.
  • Render the default table as:
    ID | TRACK | ARTIST_IDS | ARTISTS | ALBUM_ID | ALBUM | DURATION
  • Put continuation notices on stderr and rows on stdout.
  • Represent opt-in artwork compactly as dimensions plus Spotify URL; never download it.

Acceptance

  • Ordinary, field-qualified, Unicode, empty-result, paginated, ID-only, projected, extended, and artwork searches are covered.
  • Album and artist breadcrumb IDs are present by default.
  • --id emits only track IDs and overrides other output-shape flags.
  • Exact stdout/stderr rendering is locked with tests.
  • Retry and terminal failure behavior are deterministic under httptest.

Activity

  1. rianjs commented on Jul 22, 2026

    @rianjs
    ContributorAuthor

    Plan: issue #7 — token-efficient track search

    Architectural intent

    Keep Spotify-specific behavior concrete and preserve the existing three seams: typed API data, pure presentation, and thin Cobra orchestration. The second authenticated command earns extraction of credential/config/token-source setup, but nothing broader: no provider interface, repository layer, Spotify SDK, generic resource framework, generic pagination framework, or dynamic projection engine.

    1. Extract the one shared authenticated-session seam

    • Add internal/session with a concrete, request-scoped opener that owns the existing repeated sequence: load and validate config, resolve/open the selected credential backend, read and strictly decode the OAuth envelope, create the persistent refresh token source, and return an authenticated concrete client.Client, scopes, and close-only lifecycle.
    • Open accepts the command's context.Context and supplies that same context to the persistent token source and OAuth client so cancellation interrupts both refresh and API work.
    • Keep Cobra and exit-code policy out of this package. Accept an already-validated backend value/explicitness as inputs and return secret-safe errors. Keep the credential store private inside the session/persistence closure; expose only client, copied scopes, and Close (with a small internal constructor usable by command tests).
    • Preserve backend classification at the command boundary: an explicit invalid or memory backend is a usage error and is rejected before opening a store; a supported but unavailable backend and all other session-opening failures are configuration errors.
    • Construct the opener once in internal/cmd/root from the already-injected runtime effects and pass its narrow Open function to authenticated commands.
    • Refactor me to consume the shared opener while retaining its user-read-private scope check, JSON control-plane option, output, error classification, and refresh persistence behavior. Leave init verification separate because it verifies a newly issued, not-yet-persisted envelope.
    • Move session lifecycle/refresh assertions to focused session tests and retain only me-specific rendering, required-scope, and error-routing coverage in mecmd; delete tests made exactly redundant by the extraction.

    Verification: session tests prove missing/invalid credential failures are secret-safe, backend selection reaches the opener, refresh persists to the same credential, close occurs, and cancellation interrupts a blocked refresh; command tests prove invalid/test-only backends do not call the session opener while unavailable supported backends remain config errors; existing me and root integration tests remain green.

    2. Add the typed search client and bounded retry path

    • Extend internal/client with Spotify-specific Track, Artist, Album, Image, and TrackPage values containing only fields needed by this command.
    • Add SearchTracks(ctx, query, limit, offset) using GET /search, type=track, and net/url query encoding. Decode the Spotify paging envelope into typed values; treat a missing/malformed envelope or a response over the existing 1 MiB bound as ErrInvalidResponse.
    • Centralize the common authenticated GET/status/bounded-JSON path so authorization and response-body details never enter returned messages. Preserve context cancellation. Keep 401/403 terminal and classified as today.
    • Retry only 429/502/503/504, with three total attempts. Honor non-negative integer-seconds Retry-After for 429 only when it parses without overflow and is at most a fixed five-minute safety ceiling; missing, malformed, negative, overflowing, or excessive values fail terminally without sleeping. Use a small fixed/exponential delay for the transient 5xx statuses. Inject only the wait function needed for deterministic tests; production uses a context-aware timer. Do not add configurable retry policies.
    • Ignore Spotify's next URL as a request target. Return enough paging metadata for the command to derive the next offset locally.

    Verification: httptest cases assert exact method/path/query encoding (including field-qualified and Unicode queries), typed breadcrumb decoding, empty results, bounded/malformed responses, cancellation, terminal 401/403/non-retryable failures, exact retry count/delays for 429 and transient 5xx, all invalid/excessive Retry-After forms, and safe errors that contain neither Authorization canaries nor body canaries.

    3. Add pure track projection and token-efficient rendering

    • Add a focused track presenter under internal/output; use a small static field list and switch, not the adjacent CLI's dynamic registry.
    • Default to exactly ID, TRACK, ARTIST_IDS, ARTISTS, ALBUM_ID, ALBUM, DURATION. Render multiple artist IDs/names comma-separated and duration as m:ss or h:mm:ss.
    • Define extended columns for the less-frequent data already decoded: URI, URL, DISC_NUMBER, TRACK_NUMBER, EXPLICIT, RESTRICTION. --extended appends them to defaults.
    • Define ARTWORK as an opt-in column containing comma-separated <width>x<height> <url> entries in Spotify order. --include-artwork appends it to the widened/default set; explicit --fields replaces the full selection and can name ARTWORK directly.
    • Parse --fields case-insensitively, trim and de-duplicate selections while preserving first-seen order, make empty CSV fall back to the widened/default set, and reject unknown names with the valid list. --id short-circuits projection parsing and emits one ID per line without a header.
    • Render pipe tables with ALL_CAPS headers, - for empty cells, and one row per line. Replace CR/LF and embedded structural | sequences with one space while leaving naked pipes intact; document this substitution in command help or README.

    Verification: pure exact-string tests cover default breadcrumbs, multi-artist/duration formatting, empty result, ID-only precedence, field projection/order/errors, extended columns, artwork, empty values, and structural-cell sanitization.

    4. Add search track command orchestration and CLI-owned page tokens

    • Add internal/cmd/searchcmd with a search group and track <query> leaf, then wire it in internal/cmd/root using the shared session opener.
    • Register -m, --max with default/ceiling 10 and a help explanation of the endpoint-specific cap, plus long-only --next-page-token, --id, --fields, --extended, and --include-artwork.
    • Before opening credentials or making HTTP requests, require exactly one non-blank query, validate 1 <= max <= 10, resolve the output projection (unless --id short-circuits it), and decode the page token.
    • Keep the page token local to this command: raw-URL-base64 encode a compact versioned track payload containing only the next offset. Validate version, surface, shape, integer, and Spotify's inclusive 0..1000 offset range before I/O. Do not sign it: offset is not a trust boundary, and every decoded value is locally bounded.
    • Validate returned paging metadata against the request, derive continuation only from the validated response offset/limit plus the presence of provider continuation, and emit a token only when the derived offset remains within 0..1000. Never emit a token that the next invocation would reject.
    • Call the concrete authenticated client, write rendered rows only to stdout, and when Spotify reports a next page write exactly More results available (next: <token>) plus newline to stderr. Never request the provider-supplied next URL.
    • Classify session/opening and 401/403/refresh failures as config errors, input/token/projection failures as usage errors, and other client failures as upstream errors.

    Verification: command tests use a real httptest server plus a narrow fake session opener to prove invalid input makes zero opener/network calls; exact stdout/stderr for ordinary, empty, paginated, ID-only, projected, extended, and artwork results; offset-1000 and inconsistent-metadata boundaries; and Authorization-bearing end-to-end request wiring. Add only one root-level assertion that the command is registered and production composition reaches it, avoiding duplication of command tests.

    5. Documentation and full verification

    • Update README command examples/output documentation with search track, its 10-result cap, shape-flag precedence, breadcrumb columns, pagination notice, and the documented cell substitution. Do not document internal token structure.
    • Run gofmt, go test ./..., no-1Password-tag tests, static-smoke tests, golangci-lint run, go mod tidy with a clean module diff, build, and the repository's full make check.
    • Exercise the built binary's help and representative validation failures to confirm no credentials/network are touched and stdout/stderr remain separated.
    • Commit only issue feat: add token-efficient track search #7 files as feat: add token-efficient track search with Closes #7 in the body, push the cumulative branch, and update draft PR feat: ship initial Spotify CLI vertical slice #9/issue feat: add token-efficient track search #7 only after the required review loops converge.
  2. added 5 commits that reference this issue on Jul 22, 2026
    5d1d6a2
    32b608e
    235571b
    1de5ead
    9b33314
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions