waxtap

package module
v2.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 33 Imported by: 0

README

WaxTap

WaxTap downloads and processes YouTube audio. It ships as a Go library and the waxtap CLI, both on the same processing core. Processing is opt-in: transcode, cut time ranges, drop SponsorBlock segments, measure or normalize loudness. A plain download keeps the selected source stream without re-encoding.

WaxTap targets public videos. Private, age-restricted, and login-gated videos are expected failures, not bypass targets. YouTube changes without notice; see MAINTENANCE.md when extraction breaks.

Highlights

  • Pure-Go extraction via InnerTube and goja. No yt-dlp dependency.
  • Token-free ANDROID_VR is the default. Full WEB audio is opt-in and needs an attested identity; forced iOS delivery is best-effort.
  • One ffmpeg pass can combine cuts, SponsorBlock removal, normalization, and transcoding.
  • Lossless output such as FLAC is still a re-encode of YouTube's lossy source. Only copy/remux avoids re-encoding.

The stable facade is the root waxtap package. YouTube code is isolated under youtube; processing lives in cut, normalize, transcode, and internal/pipeline.

Requirements

  • Go 1.26+
  • ffmpeg and ffprobe for transcoding, cutting, normalization, and probing. Metadata and keep-source downloads do not need them.

Install

go install github.com/colespringer/waxtap/v2/cmd/waxtap@latest   # CLI
go get github.com/colespringer/waxtap/v2                         # library

Release archives hold Linux, macOS, and Windows binaries for amd64 and arm64. Put waxtap on PATH and run waxtap --help. Unsigned macOS binaries may need xattr -d com.apple.quarantine /path/to/waxtap; Windows may prompt SmartScreen.

CLI

Media commands accept a YouTube URL or bare video or playlist ID. download also accepts a channel URL or bare UC ID, resolving to the channel's uploads feed. cut, transcode, and normalize also take local files. Every command has --help, and --json is a stable scriptable contract (schemaVersion 1).

waxtap info <video-url>                         # metadata and best audio
waxtap formats <video-url>                      # all audio formats
waxtap download <video-url> -o track            # keep source
waxtap download <video-url> --format flac -o track.flac
waxtap download <video-url> --sponsorblock --normalize --format mp3 -o track.mp3

waxtap transcode song.flac song.mp3
waxtap normalize song.wav --loudness-target -14 --format flac -o song.flac
waxtap normalize --album --format flac --dir ./normalized ./album/*.flac

waxtap download <playlist-url> -d ./music --download-archive archive.txt
waxtap download <channel-url> -d ./music        # channel uploads, newest first
waxtap download <channel-url> --list            # list entries, no download
waxtap doctor

info --show-url adds a signed, expiring stream URL and content length under resolved.*; treat that output as sensitive. info --full adds publish date and chapters via a token-free watch-page fetch.

transcode and normalize also take directories: -r recurses, --dir sets an output directory, and --force re-encodes files already in the target codec. Album normalization preserves relative track loudness. Loudness uses EBU R128 (integrated LUFS, true peak dBTP, range LU).

Notes
  • --channels mono|stereo|surround|any picks a native layout, defaulting to stereo. --downmix allows surround-to-stereo/mono; it never upmixes.
  • --no-fallback disables watch-page, WEB-context, and incomplete-download fallbacks. Results report the client that actually delivered.
  • Playlist downloads support --concurrency, pacing, attempt limits, collision policies, and yt-dlp-compatible --download-archive files.
  • waxtap cache dir and waxtap cache clean manage the persistent player-JS cache; --no-cache disables it.
Exit codes

The CLI maps failures to stable exit codes. The same class appears in --json as error.code. Run waxtap exit-codes for the built-in table.

Code Meaning
0 success
1 unclassified error
2 invalid request/config, including a playlist or channel URL passed to a video command, unsupported input, or unavailable requested format
3 unavailable/restricted video or playlist (private, age-restricted, members-only, geo-blocked, removed), login required, live or upcoming, or no audio
4 extraction, cipher, or playlist parsing failure; WaxTap may need an update
5 rate limited
6 ffmpeg/ffprobe not found
7 incomplete stream or expired stream URL
8 PO token required, missing, rejected, or not minted
9 network failure, including an unreachable proxy or sidecar
10 local I/O failure
130 canceled with SIGINT

Malformed targets exit 2; a well-formed but nonexistent or private video can only be classified after a network request and exits 3.

Library

package main

import (
	"context"
	"log"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	client, err := waxtap.New(waxtap.Options{})
	if err != nil {
		log.Fatal(err)
	}

	_, err = client.Download(context.Background(), waxtap.Request{
		URL: "https://youtu.be/VIDEO_ID_01",
		ProcessSpec: waxtap.ProcessSpec{
			Transcode: &waxtap.TranscodeSpec{Format: waxtap.FormatFLAC},
			Output:    waxtap.ToFile("track.flac"),
		},
	})
	if err != nil {
		log.Fatal(err)
	}
}

A default Download (nil ProcessSpec) delivers the source stream byte-for-byte: no ffmpeg, SourceBytes == OutputBytes, Transcoded false. Library selection starts from LayoutAny and can rank a surround track highest; pass WithChannels(LayoutStereo) to match the CLI. Client.Enumerate expands a playlist or channel URL with Skip/Stop predicates for an archive cursor, and WithFullMetadata() adds publish date and chapters.

Availability failures (ErrVideoUnavailable, ErrAgeRestricted, ErrMembersOnly, ErrGeoBlocked, ErrLiveContent, ErrLiveNotStarted, and siblings) are typed sentinels a feed consumer should skip rather than treat as fatal; see the package doc's skip-vs-fail taxonomy. For full WEB SABR audio, wire a sidecar through NewSidecarPOTokenProvider, NewSidecarPlayerContextProvider, or NewSidecarSessionProvider (see below). example_test.go covers streaming, local processing, playlists, SponsorBlock, album measurement, metadata, and WEB SABR.

Configuration

CLI precedence, highest to lowest: explicit flag, WAXTAP_* environment variable, JSON config file, built-in default. The default file is config.json under os.UserConfigDir()/waxtap; override with --config or WAXTAP_CONFIG. Unknown JSON keys and malformed environment values are errors.

--json, --quiet, and --verbose are global. Other flags appear only on the commands that use them. Timeout values are seconds; keys with no flag are config/environment only.

Config key Environment variable Flag
cacheDir WAXTAP_CACHE_DIR --cache-dir
noCache WAXTAP_NO_CACHE --no-cache
tempDir WAXTAP_TEMP_DIR --temp-dir
proxy WAXTAP_PROXY --proxy
insecure WAXTAP_INSECURE --insecure
perHostQPS WAXTAP_QPS --qps
cooldownSeconds WAXTAP_COOLDOWN --cooldown
hl WAXTAP_HL --hl
gl WAXTAP_GL --gl
sponsorBlockBaseURL WAXTAP_SPONSORBLOCK_BASE_URL --sponsorblock-url
profileOverridePath WAXTAP_PROFILE_OVERRIDE --profile-override
chromeMajor WAXTAP_CHROME_MAJOR --chrome-major
poTokenURL WAXTAP_POTOKEN_URL --potoken-url
playerContextURL WAXTAP_PLAYER_CONTEXT_URL --player-context-url
client WAXTAP_CLIENT --client
sessionURL WAXTAP_SESSION_URL --session-url
visitorData WAXTAP_VISITOR_DATA --visitor-data
cookies WAXTAP_COOKIES --cookies
apiKey WAXTAP_API_KEY --api-key
channels WAXTAP_CHANNELS --channels
downmix WAXTAP_DOWNMIX --downmix
downloadConcurrency WAXTAP_DOWNLOAD_CONCURRENCY --concurrency (download)
ffmpegProcs WAXTAP_FFMPEG_PROCS -
chunkParallelism WAXTAP_CHUNKS -
extractionTimeoutSeconds WAXTAP_EXTRACTION_TIMEOUT -
resolveTimeoutSeconds WAXTAP_RESOLVE_TIMEOUT -
webContextTimeoutSeconds WAXTAP_WEB_CONTEXT_TIMEOUT -
sponsorBlockTimeoutSeconds WAXTAP_SPONSORBLOCK_TIMEOUT -
chunkTimeoutSeconds WAXTAP_CHUNK_TIMEOUT -

PO tokens and WEB

ANDROID_VR is token-free for public videos. WEB-family clients use URL-less SABR/UMP audio, and complete delivery needs three things together: a GVS-scope PO-token provider (Options.POTokenProvider or --potoken-url), an attested identity (a /player-context handoff or an adopted /session), and a shared egress IP for the attesting service and the download. A PO token alone does not lift the WEB preview cap.

# Attested player context, adopted WEB session as fallback
waxtap download <url> --client web \
  --player-context-url http://127.0.0.1:4416/player-context \
  --session-url http://127.0.0.1:4416/session \
  --potoken-url http://127.0.0.1:4416/get_pot

WaxTap tries the attested player context first; if it fails or caps, the WEB chain can use the adopted session. Static adoption is also available with --visitor-data and optional --cookies. Library callers get the same handoff via the NewSidecar* providers, each taking a base URL or full endpoint plus an optional WithSidecarAPIKey; ParseNetscapeCookies loads a static session from a browser cookies.txt. See MAINTENANCE.md for sidecar contracts and SABR diagnostics.

Maintenance

waxtap doctor runs a low-cost extraction, resolution, and byte-read health check; waxtap doctor --full verifies complete delivery. The maintenance runbook covers dumps, profile refreshes, cipher failures, SABR changes, fixtures, and releases.

Acknowledgements

WaxTap was influenced by kkdai/youtube and yt-dlp, but ships no code from either and does not invoke yt-dlp.

Disclaimer

WaxTap is for personal and otherwise authorized use. You are responsible for complying with YouTube's Terms of Service and applicable law.

License

MIT.

Documentation

Overview

Package waxtap provides the public WaxTap API for acquiring and processing audio.

WaxTap can take audio from YouTube or from a local file. Processing stages such as transcoding, cutting, SponsorBlock removal, loudness measurement, and loudness normalization are opt-in. By default, downloads keep the selected source stream and do not re-encode it.

Client.Download and Client.Stream handle one video. Client.DownloadPlaylist downloads playlist entries with bounded concurrency, optional pacing, and an optional limit on download attempts. Options configures per-host request rates and post-rate-limit cooldowns.

The default client chain returns playable audio for public videos with no PO token. WEB-family clients need a POTokenProvider and remain experimental. For byte-exact session coherence with a token minter, Options.Session / Options.SessionProvider adopt an externally supplied guest visitorData and cookies verbatim instead of bootstrapping; adoption requires a uniform client chain (Options.Client or a single-family profile override) and resolves once per Client.

This top-level package is the stable public surface. The youtube package and packages below it are YouTube-specific implementation surfaces; they are exported where the facade needs them, but external callers should prefer this package.

Type ownership

To keep the dependency graph acyclic, contract types are defined in the package that owns the behavior and re-exported here for convenience:

  • audio formats and selectors: package format
  • the PO-token provider contract: package potoken
  • the SponsorBlock category vocabulary: package sponsorblock
  • the error taxonomy: package waxerr
  • extraction models (Video, Playlist): package youtube

Callers can work entirely through waxtap, using names such as BestAudio, ErrVideoUnavailable, and POTokenProvider.

Identity anchors

[Video.ID] (the 11-character video ID) and [Video.ChannelID] (the UC channel ID) are the canonical, stable YouTube identifiers. Callers that persist or deduplicate should key on these rather than on titles or URLs. [Video.URL] is the canonical watch URL derived from [Video.ID].

Availability errors: skip vs. fail

Info and Download return typed availability sentinels for videos that exist but cannot be delivered. A consumer iterating a feed should treat these as "skip this item and continue," not as a hard failure, because retrying or updating the tool will not help:

Everything else is a hard error the consumer should surface: extraction and cipher maintenance signals (ErrExtractionFailed, ErrCipherSolve, ErrPlaylistParse), rate limiting (ErrRateLimited), incomplete delivery (ErrIncompleteStream, ErrURLExpired), token preconditions (ErrNeedsPOToken), and network or I/O failures. Match sentinels with errors.Is; a PlayabilityError (via errors.As / errors.AsType) carries YouTube's Status and Reason for finer classification. Members-only and geo-blocked matching is best-effort under the default en/US locale.

Example (Errors)

Example_errors shows the two ways to inspect a failure from Download or Info: errors.Is for the sentinel category, and errors.As for YouTube's structured detail. The re-exported error types satisfy error on their pointer type, so errors.As takes a double-pointer target (var pe *PlayabilityError; &pe).

package main

import (
	"errors"
	"fmt"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	// In real use this comes from client.Download/Info; constructed here for a
	// self-contained example.
	var err error = &waxtap.PlayabilityError{
		Status:   "UNPLAYABLE",
		Reason:   "This video is unavailable",
		Sentinel: waxtap.ErrVideoUnavailable,
	}

	if errors.Is(err, waxtap.ErrVideoUnavailable) {
		fmt.Println("category: video unavailable")
	}

	var pe *waxtap.PlayabilityError
	if errors.As(err, &pe) {
		fmt.Printf("status: %s\n", pe.Status)
	}
}
Output:
category: video unavailable
status: UNPLAYABLE

Index

Examples

Constants

View Source
const (
	Unknown = format.Unknown
	Yes     = format.Yes
	No      = format.No
)

Tri values.

View Source
const (
	QualityUnknown  = format.QualityUnknown
	QualityUltraLow = format.QualityUltraLow
	QualityLow      = format.QualityLow
	QualityMedium   = format.QualityMedium
	QualityHigh     = format.QualityHigh
)

Audio quality tiers reported by YouTube.

View Source
const (
	LayoutAny      = format.LayoutAny
	LayoutMono     = format.LayoutMono
	LayoutStereo   = format.LayoutStereo
	LayoutSurround = format.LayoutSurround
)

Channel layouts used by AudioSelector.WithChannels and ProcessSpec.Channels. LayoutAny is the neutral zero value.

View Source
const (
	CategorySponsor       = sponsorblock.CategorySponsor
	CategorySelfPromo     = sponsorblock.CategorySelfPromo
	CategoryInteraction   = sponsorblock.CategoryInteraction
	CategoryIntro         = sponsorblock.CategoryIntro
	CategoryOutro         = sponsorblock.CategoryOutro
	CategoryPreview       = sponsorblock.CategoryPreview
	CategoryFiller        = sponsorblock.CategoryFiller
	CategoryMusicOffTopic = sponsorblock.CategoryMusicOffTopic
)

SponsorBlock categories. Values match the SponsorBlock API wire strings.

View Source
const (
	LiveNone     = youtube.LiveNone
	LiveUpcoming = youtube.LiveUpcoming
	LiveNow      = youtube.LiveNow
	LiveWasLive  = youtube.LiveWasLive
)

LiveStatus values.

View Source
const (
	AvailabilityUnknown  = youtube.AvailabilityUnknown
	AvailabilityPublic   = youtube.AvailabilityPublic
	AvailabilityUnlisted = youtube.AvailabilityUnlisted
)

Availability values.

View Source
const (
	ScopeNone      = potoken.ScopeNone      // no token scope
	ScopePlayer    = potoken.ScopePlayer    // /player request body
	ScopeGVS       = potoken.ScopeGVS       // googlevideo media URL
	ScopeSubtitles = potoken.ScopeSubtitles // subtitle or timed-text URL
)

PO-token scopes identify where a token will be used. Tokens are not interchangeable across scopes.

Variables

View Source
var (
	// YouTube extraction maintenance signals.
	ErrExtractionFailed = waxerr.ErrExtractionFailed
	ErrCipherSolve      = waxerr.ErrCipherSolve
	ErrNeedsPOToken     = waxerr.ErrNeedsPOToken
	ErrURLExpired       = waxerr.ErrURLExpired
	// ErrIncompleteStream indicates that a client returned a detectably truncated
	// stream. Another client may still deliver the complete stream.
	ErrIncompleteStream = waxerr.ErrIncompleteStream

	// Availability failures.
	ErrVideoUnavailable = waxerr.ErrVideoUnavailable
	ErrVideoRestricted  = waxerr.ErrVideoRestricted
	ErrLoginRequired    = waxerr.ErrLoginRequired
	// ErrLiveContent indicates a stream that is currently live; ErrLiveNotStarted
	// indicates an upcoming/premiere or offline stream that may become available.
	ErrLiveContent    = waxerr.ErrLiveContent
	ErrLiveNotStarted = waxerr.ErrLiveNotStarted
	// ErrAgeRestricted, ErrMembersOnly, and ErrGeoBlocked are specific availability
	// verdicts. The default ANDROID_VR client bypasses age-gating, so
	// ErrAgeRestricted is near-unreachable unless a stricter client is forced.
	ErrAgeRestricted  = waxerr.ErrAgeRestricted
	ErrMembersOnly    = waxerr.ErrMembersOnly
	ErrGeoBlocked     = waxerr.ErrGeoBlocked
	ErrNoAudioFormats = waxerr.ErrNoAudioFormats
	// ErrRequestedFormatUnavailable indicates an explicit itag/codec selector
	// matched none of the available audio formats. It is distinct from
	// ErrNoAudioFormats, which means no usable audio formats exist.
	ErrRequestedFormatUnavailable = waxerr.ErrRequestedFormatUnavailable

	// Throttling.
	ErrRateLimited = waxerr.ErrRateLimited

	// Input / routing.
	ErrIsPlaylist = waxerr.ErrIsPlaylist
	// ErrIsChannel indicates a channel URL was passed where a single video is
	// required.
	ErrIsChannel      = waxerr.ErrIsChannel
	ErrInvalidVideoID = waxerr.ErrInvalidVideoID
	// ErrVideoIDTooShort and ErrVideoIDTooLong indicate an all-ID-character token
	// of the wrong length (a video ID is exactly 11 characters).
	ErrVideoIDTooShort   = waxerr.ErrVideoIDTooShort
	ErrVideoIDTooLong    = waxerr.ErrVideoIDTooLong
	ErrInvalidPlaylistID = waxerr.ErrInvalidPlaylistID
	// ErrPlaylistParse is a maintenance signal, not a bad input: the playlist
	// response parsed but matched no known shape.
	ErrPlaylistParse = waxerr.ErrPlaylistParse
	// ErrPlaylistUnavailable indicates that a playlist is private, deleted, or
	// otherwise inaccessible. See [PlaylistUnavailableError].
	ErrPlaylistUnavailable = waxerr.ErrPlaylistUnavailable
	// ErrPlaylistEmpty indicates that a valid playlist contains no videos.
	ErrPlaylistEmpty = waxerr.ErrPlaylistEmpty
	// ErrShortsPlaylist indicates that WaxTap cannot enumerate a channel's Shorts
	// shelf playlist. It wraps [ErrUnsupportedInput].
	ErrShortsPlaylist = waxerr.ErrShortsPlaylist

	// Processing / local files.
	ErrIncompatibleSpec = waxerr.ErrIncompatibleSpec
	ErrUnsupportedInput = waxerr.ErrUnsupportedInput
	ErrFFmpegNotFound   = waxerr.ErrFFmpegNotFound

	// ErrInvalidConfig indicates invalid or conflicting library configuration.
	ErrInvalidConfig = waxerr.ErrInvalidConfig
)

Re-exported sentinel errors. The canonical definitions live in package waxerr; match them with errors.Is.

View Source
var DefaultCategories = sponsorblock.DefaultCategories

DefaultCategories contains the categories used when CutSpec.SponsorBlock is a non-nil empty slice.

View Source
var ErrNoMatch = format.ErrNoMatch

ErrNoMatch reports that audio selection found no candidate satisfying the request. Download/Process translate it to ErrNoAudioFormats; it is re-exported for callers using BestForTarget directly.

Functions

func BestForTarget

func BestForTarget(candidates []Format, policy SourcePolicy, target Target) (int, error)

BestForTarget chooses the best source audio index for a transcode target under a SourcePolicy. It is the selection BestAudio uses; exposed for callers that resolve formats themselves.

func ParseNetscapeCookies

func ParseNetscapeCookies(path string) ([]*http.Cookie, error)

ParseNetscapeCookies reads a Netscape/Mozilla cookies.txt file (the format yt-dlp and curl use) into http.Cookies. The returned slice matches POTokenSession.Cookies, so a caller can adopt a static session from a browser-exported cookies.txt without reimplementing the format.

Each data line is seven tab-separated fields: domain, include-subdomains flag, path, secure, expiry (unix seconds; 0 = session), name, value. Some exporters drop the trailing tab when the value is empty, leaving six fields; such a line is read with an empty value rather than skipped. The "#HttpOnly_" domain prefix is checked before comment skipping, since those lines are real cookies marked HttpOnly, not comments. Blank lines, ordinary "#" comments, and malformed (under-six-field) lines are skipped.

func ValidateProcessSpec

func ValidateProcessSpec(s ProcessSpec) error

ValidateProcessSpec checks a ProcessSpec without acquiring or processing media. Invalid specs return an error that wraps ErrIncompatibleSpec. Client.Download, Client.Stream, and Client.Process call it automatically; callers may use it to fail before starting batch work.

Types

type AlbumLoudnessResult

type AlbumLoudnessResult struct {
	Album    LoudnessInfo   // loudness measured across the complete album
	PerTrack []LoudnessInfo // measurements in input order
}

AlbumLoudnessResult reports a group loudness measurement plus per-track measurements, in input order. The album value is a true group EBU R128 measurement, not a mean of the per-track LUFS.

type AlbumProcessResult

type AlbumProcessResult struct {
	Album    LoudnessInfo   // loudness measured across the complete album
	GainDB   float64        // Target - album integrated LUFS, applied to every track (0 for a silent album)
	PerTrack []LoudnessInfo // input measurements in track order
	Outputs  []string       // completed output paths in track order
}

AlbumProcessResult reports the album loudness, the gain applied to every track, the input measurements, and the output paths.

type AlbumTrack

type AlbumTrack struct {
	Input  string // source file path
	Output string // destination file path
}

AlbumTrack names one album input and where its processed output should be written.

type AudioQualityTier

type AudioQualityTier = format.AudioQualityTier

Audio format model and selectors (package format).

type AudioSelector

type AudioSelector = format.AudioSelector

Audio format model and selectors (package format).

func BestAudio

func BestAudio() AudioSelector

BestAudio selects the best audio stream. It prefers the original track, non-DRC audio, higher reported quality tiers, Opus within a tier, and finally higher effective bitrate.

The selector itself imposes no channel preference, but the Download, Info, and Resolve facades apply a stereo default to it, so client.Download(Request{URL}) yields stereo. Call WithChannels(LayoutSurround) for surround, or WithChannels(LayoutAny) to let a surround track rank highest.

func Codec

func Codec(codec string) AudioSelector

Codec selects the best stream whose codec matches (e.g. "opus", "aac").

func Itag

func Itag(itag int) AudioSelector

Itag selects the stream with the exact itag.

type AudioTrack

type AudioTrack = format.AudioTrack

Audio format model and selectors (package format).

type Availability

type Availability = youtube.Availability

Availability reports whether a video is publicly listed. It is set only when a watch-page metadata pass runs (see WithFullMetadata), else AvailabilityUnknown.

type Category

type Category = sponsorblock.Category

Category identifies a SponsorBlock segment category.

type ChannelLayout

type ChannelLayout = format.ChannelLayout

Audio format model and selectors (package format).

type Chapter

type Chapter = youtube.Chapter

Extraction models (package youtube). Part of the volatile surface; may evolve pre-1.0.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the main WaxTap entry point for library callers and the CLI. It is safe for concurrent use after construction.

The same httpx.Client backs extraction, media download, and SponsorBlock. Its per-host limiter is shared by all request paths, while each host keeps its own schedule. The ffmpeg Runner is created lazily on first use, so metadata calls and keep-source downloads work without ffmpeg installed.

func New

func New(opts Options) (*Client, error)

New constructs a Client from Options, applying defaults for unset fields.

func (*Client) Download

func (c *Client) Download(ctx context.Context, req Request) (res *Result, err error)

Download acquires and processes a single YouTube video to the configured sink. It is strictly single-video: a playlist URL returns ErrIsPlaylist (use Enumerate and loop).

Audio selection defaults to stereo, so a bare Request (zero Audio) yields the best stereo track rather than a surround one. Set Audio: BestAudio().WithChannels(LayoutSurround) for surround, or WithChannels(LayoutAny) to rank purely by fidelity.

When no processing is requested (a nil ProcessSpec) it downloads the selected source stream straight to the sink with no ffmpeg and no temp file: the bytes are byte-identical to what YouTube served, so Result.SourceBytes == Result.OutputBytes, Result.OutputFormat == Result.SourceFormat, and Result.Transcoded is false. A TranscodeSpec with FormatCopy is different: it stream-copies through ffmpeg to remux into the target container, so no re-encode happens but the bytes and container may change. When a cut, transcode, or loudness stage is requested it stages the source to a temp file, runs the fused pipeline, and finalizes to the sink.

Example

ExampleClient_Download downloads the best audio stream to a file. With no processing requested, WaxTap keeps the source encoding.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	client, err := waxtap.New(waxtap.Options{})
	if err != nil {
		log.Fatal(err)
	}

	res, err := client.Download(context.Background(), waxtap.Request{
		URL: "https://www.youtube.com/watch?v=VIDEO_ID_01",
		ProcessSpec: waxtap.ProcessSpec{
			Output: waxtap.ToFile("track.opus"),
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s -> %s (%d bytes)\n", res.VideoID, res.OutputPath, res.OutputBytes)
}
Example (TranscodeAndSponsorBlock)

ExampleClient_Download_transcodeAndSponsorBlock downloads a video, removes SponsorBlock "music_offtopic" segments, and transcodes to FLAC in one ffmpeg pass. SourcePolicy defaults to MinimizeLoss.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	client, err := waxtap.New(waxtap.Options{})
	if err != nil {
		log.Fatal(err)
	}

	res, err := client.Download(context.Background(), waxtap.Request{
		URL: "https://www.youtube.com/watch?v=VIDEO_ID_01",
		ProcessSpec: waxtap.ProcessSpec{
			Transcode: &waxtap.TranscodeSpec{Format: waxtap.FormatFLAC},
			Cut: &waxtap.CutSpec{
				SponsorBlock: []waxtap.Category{waxtap.CategoryMusicOffTopic},
				OnError:      waxtap.ProceedUncut,
			},
			Output: waxtap.ToFile("track.flac"),
			Events: func(e waxtap.Event) {
				if e.Stage == waxtap.StageWarning && e.Warning != nil {
					log.Printf("warning: %s", e.Warning.Detail)
				}
			},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("cut=%v transcoded=%v\n", res.CutApplied, res.Transcoded)
}

func (*Client) DownloadPlaylist

func (c *Client) DownloadPlaylist(ctx context.Context, url string, o PlaylistDownloadOptions) (*PlaylistRunResult, error)

DownloadPlaylist enumerates a playlist URL and downloads its entries with bounded concurrency, optional pacing, and an optional attempt limit.

An enumeration failure returns an error and a nil result. Item-level enumeration errors do not stop downloads and are returned in EnumErrors. After enumeration succeeds, cancellation returns a partial result with ctx.Err().

Example

ExampleClient_DownloadPlaylist downloads up to ten playlist entries one at a time, waiting between downloads. BuildRequest prepares or skips each entry; OnItem receives the outcome for every entry the run reaches.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	client, err := waxtap.New(waxtap.Options{})
	if err != nil {
		log.Fatal(err)
	}

	res, err := client.DownloadPlaylist(context.Background(),
		"https://www.youtube.com/playlist?list=UUSMOQeBJ2RAnuFungnQOxLg",
		waxtap.PlaylistDownloadOptions{
			Concurrency:   1,               // serialize downloads
			SleepInterval: 5 * time.Second, // pause between them
			MaxDownloads:  10,              // stop after 10 attempts
			BuildRequest: func(_ context.Context, e waxtap.PlaylistEntry) (waxtap.Request, string, error) {
				return waxtap.Request{
					URL:         e.VideoID,
					ProcessSpec: waxtap.ProcessSpec{Output: waxtap.ToFile(e.VideoID + ".opus")},
				}, "", nil
			},
			OnItem: func(o waxtap.PlaylistItemOutcome) {
				switch {
				case o.Err != nil:
					log.Printf("%s: %v", o.Entry.VideoID, o.Err)
				case o.SkipReason != "":
					log.Printf("%s: skipped (%s)", o.Entry.VideoID, o.SkipReason)
				}
			},
		},
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d downloaded, %d remaining (cap reached: %v)\n",
		res.Downloaded, res.Remaining, res.CapReached)
}

func (*Client) Enumerate

func (c *Client) Enumerate(ctx context.Context, url string, opts EnumerateOptions) (*Playlist, error)

Enumerate expands a playlist or channel URL into entries without downloading media. A channel reference (a bare UC ID, or a /channel/, /@handle, /c/, or /user/ URL, with any trailing tab stripped) resolves to the channel's uploads feed, which is newest-first and lists Shorts and past live streams as ordinary entries. EnumerateOptions.MaxItems caps the listing, and Skip/Stop drive an archive cursor. With Enrich set, InfoBasic calls refresh entries at bounded concurrency. Successful calls update their entries; item-level failures are added to Playlist.Errors.

Example

ExampleClient_Enumerate lists a playlist without downloading any audio.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	client, err := waxtap.New(waxtap.Options{})
	if err != nil {
		log.Fatal(err)
	}

	pl, err := client.Enumerate(context.Background(),
		"https://www.youtube.com/playlist?list=UUSMOQeBJ2RAnuFungnQOxLg",
		waxtap.EnumerateOptions{MaxItems: 50},
	)
	if err != nil {
		log.Fatal(err)
	}
	for _, entry := range pl.Entries {
		// The video ID is the stable key WaxTap uses for deduplication.
		fmt.Printf("%d. %s (%s)\n", entry.Index, entry.Title, entry.VideoID)
	}
}

func (*Client) Info

func (c *Client) Info(ctx context.Context, url string, depth InfoDepth, opts ...ReadOption) (*Video, error)

Info returns video metadata and candidate audio formats at the requested depth, without downloading.

Example

ExampleClient_Info fetches metadata and candidate audio formats without downloading.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	client, err := waxtap.New(waxtap.Options{})
	if err != nil {
		log.Fatal(err)
	}

	video, err := client.Info(context.Background(), "VIDEO_ID_01", waxtap.InfoBasic)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s by %s (%d formats)\n", video.Title, video.Author, len(video.Formats))
}

func (*Client) InfoResult

func (c *Client) InfoResult(ctx context.Context, url string, depth InfoDepth, opts ...ReadOption) (*InfoResult, error)

InfoResult returns video metadata, candidate audio formats, and the extraction client at the requested depth, without downloading.

InfoBasic returns extracted metadata and candidate formats. InfoResolved additionally resolves the best-audio format, surfacing resolution errors (such as ErrNeedsPOToken) and filling in its content length. InfoProbe additionally runs ffprobe on that resolved stream and fills its authoritative sample rate, channel count, bitrate, and duration (network-expensive, and requires ffmpeg). The signed stream URLs themselves are not returned through Video; use Download or Stream to fetch bytes.

func (*Client) Measure

func (c *Client) Measure(ctx context.Context, path string) (LoudnessInfo, error)

Measure reports EBU R128 integrated loudness for a single local audio file. It uses Process with a measure-only spec and no Output, so no output or scratch file is created.

It requires ffmpeg. Use MeasureAlbum to measure several files as one album, or Process with a LoudnessApply spec to normalize and write audio.

Example

ExampleClient_Measure reports the loudness of a single local file without writing any output.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	client, err := waxtap.New(waxtap.Options{})
	if err != nil {
		log.Fatal(err)
	}

	loud, err := client.Measure(context.Background(), "song.flac")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%.1f LUFS\n", loud.IntegratedLUFS)
}

func (*Client) MeasureAlbum

func (c *Client) MeasureAlbum(ctx context.Context, paths []string) (*AlbumLoudnessResult, error)

MeasureAlbum measures local audio files as one album and also returns each track's loudness. It does not write output files; callers can use the album value for ReplayGain tags or playback gain.

It requires ffmpeg. Use ProcessAlbum to measure the album and write normalized tracks. Callers that manage ffmpeg directly can build the gain filter with normalize.AlbumGainFilter.

Example

ExampleClient_MeasureAlbum measures several files as one album, useful for ReplayGain-style album tags without rewriting the files.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	client, err := waxtap.New(waxtap.Options{})
	if err != nil {
		log.Fatal(err)
	}

	album, err := client.MeasureAlbum(context.Background(), []string{
		"01.flac", "02.flac", "03.flac",
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("album: %.2f LUFS\n", album.Album.IntegratedLUFS)
	for i, tr := range album.PerTrack {
		fmt.Printf("track %d: %.2f LUFS\n", i+1, tr.IntegratedLUFS)
	}
}

func (*Client) ProbeCodec

func (c *Client) ProbeCodec(ctx context.Context, path string) (string, error)

ProbeCodec reports the codec name of the first audio stream in a local file, such as "opus" or "aac". It returns ErrUnsupportedInput when the file has no audio stream.

func (*Client) Process

func (c *Client) Process(ctx context.Context, req ProcessRequest) (res *Result, err error)

Process runs the transcode/cut/normalize pipeline on a local file, with no YouTube access, through the same source-agnostic pipeline as Download. SponsorBlock is not used here: it is keyed by video ID, which a local file does not have, so only explicit Cut.Ranges apply.

The input is validated up front (ffprobe); a corrupt or non-audio file fails with ErrUnsupportedInput. Writing the output over the input is rejected unless the caller targets a different path.

Callers may omit Output only for pure loudness measurement: LoudnessMeasureOnly with no transcode, downmix, or cut. Client.Measure wraps that case.

Example

ExampleClient_Process transcodes a local file and normalizes its loudness to -14 LUFS, fused into the same encode. No YouTube access occurs.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	client, err := waxtap.New(waxtap.Options{})
	if err != nil {
		log.Fatal(err)
	}

	res, err := client.Process(context.Background(), waxtap.ProcessRequest{
		Input: "song.wav",
		ProcessSpec: waxtap.ProcessSpec{
			Transcode: &waxtap.TranscodeSpec{Format: waxtap.FormatMP3},
			Loudness:  &waxtap.LoudnessSpec{Mode: waxtap.LoudnessApply, Target: -14},
			Output:    waxtap.ToFile("song.mp3"),
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	if res.Loudness != nil && res.Loudness.Output != nil {
		fmt.Printf("normalized to %.1f LUFS\n", res.Loudness.Output.IntegratedLUFS)
	}
}

func (*Client) ProcessAlbum

func (c *Client) ProcessAlbum(ctx context.Context, tracks []AlbumTrack, target float64, spec TranscodeSpec) (*AlbumProcessResult, error)

ProcessAlbum measures local files as one album, then applies the same gain to every track. The shared offset preserves track-to-track loudness differences; per-track normalization would flatten them.

Album processing requires ffmpeg and a non-copy transcode format. A silent album applies a no-op gain, leaving each track unchanged apart from re-encoding.

func (*Client) Resolve

func (c *Client) Resolve(ctx context.Context, url string, sel AudioSelector, opts ...ReadOption) (ResolvedStream, error)

Resolve selects and resolves an audio stream without downloading it. The zero AudioSelector means best audio, defaulting to stereo like Download; pass BestAudio().WithChannels(LayoutSurround) for surround or WithChannels(LayoutAny) for any-fidelity. Direct streams include a temporary googlevideo URL and its request metadata. SABR streams set IsSABR and leave URL empty.

It is exposed for diagnostics: the CLI's info --show-url and doctor. Most callers use Download or Stream, which never expose the raw URL.

func (*Client) SponsorBlockSegments

func (c *Client) SponsorBlockSegments(ctx context.Context, videoURL string, categories []Category) ([]Segment, error)

SponsorBlockSegments returns skip segments for videoURL using the client's SponsorBlock settings and shared HTTP client. An empty categories slice uses DefaultCategories. The method does not cut or download media.

func (*Client) Stream

func (c *Client) Stream(ctx context.Context, req Request) (rc io.ReadCloser, info StreamInfo, err error)

Stream acquires a single YouTube video and returns a reader for source-style delivery (pipe to disk or object storage). When processing is requested it stages and processes to a temp file first, then streams the result. Final byte counts are known only after the reader is drained and closed.

Example

ExampleClient_Stream pipes the audio to an arbitrary writer (here a file) without staging to a temp file when no processing is requested.

package main

import (
	"context"
	"io"
	"log"
	"os"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	client, err := waxtap.New(waxtap.Options{})
	if err != nil {
		log.Fatal(err)
	}

	rc, info, err := client.Stream(context.Background(), waxtap.Request{
		URL: "https://youtu.be/VIDEO_ID_01",
	})
	if err != nil {
		log.Fatal(err)
	}
	defer rc.Close()

	out, err := os.Create("track" + "." + info.Format.Extension)
	if err != nil {
		log.Fatal(err)
	}
	defer out.Close()
	if _, err := io.Copy(out, rc); err != nil {
		log.Fatal(err)
	}
}

type Concurrency

type Concurrency struct {
	// Downloads is the max simultaneous downloads (e.g. across a playlist run).
	Downloads int
	// Chunks is the max parallel ranged chunks within a single download. Kept
	// low by default, especially for CLI playlist runs.
	Chunks int
	// FFmpeg limits concurrent ffmpeg/ffprobe processes, guarding local CPU
	// independently from network parallelism. Zero selects a conservative default
	// (GOMAXPROCS); a negative value disables the limit.
	FFmpeg int
}

Concurrency bounds parallel work. Zero values select conservative defaults at New time.

type CutMode

type CutMode uint8

CutMode selects how cuts are rendered.

const (
	// CutSmart copies when cutting alone (lossless, frame-boundary) and fuses the
	// cut into the transcode when one is requested. It avoids cut-then-transcode
	// workflows that would encode twice.
	CutSmart CutMode = iota
	// CutCopy forces stream-copy; it errors with ErrIncompatibleSpec when copy is
	// unsafe for the codec/container.
	CutCopy
	// CutAccurate decodes, cuts sample-exactly, and re-encodes.
	CutAccurate
)

type CutSpec

type CutSpec struct {
	// Ranges are explicit [Start, End) removals (optional). They are clamped to the
	// media duration. A request whose ranges all lie outside the media returns
	// ErrIncompatibleSpec; partial overlaps remain valid.
	Ranges []TimeRange
	// SponsorBlock lists categories to fetch and remove. Nil disables
	// SponsorBlock; a non-nil empty slice uses [DefaultCategories].
	SponsorBlock []Category
	// Mode selects copy/accurate/smart rendering.
	Mode CutMode
	// Crossfade, when > 0, applies a click-free crossfade at splice points. It
	// is OFF by default and orthogonal to Mode (accurate does not imply it).
	Crossfade time.Duration
	// OnError governs the SponsorBlock fetch only.
	OnError SponsorBlockErrorPolicy
	// Timeout is a strict cap on the SponsorBlock fetch.
	Timeout time.Duration
}

CutSpec describes time-range removal and/or SponsorBlock-driven cuts.

type EnumerateOptions

type EnumerateOptions struct {
	// MaxItems caps the number of entries returned (0 = all).
	MaxItems int
	// Enrich refreshes entries with InfoBasic calls made at bounded concurrency.
	// Successful calls update their entries; failures are added to Playlist.Errors.
	Enrich bool

	// Skip omits entries whose video ID it matches while continuing to page, for an
	// archive cursor. The consumer owns persistence: pass a predicate that reads
	// your store. It is safe on any playlist (seen items may be interspersed) and
	// runs before MaxItems, so the cap counts unseen entries. Skipped entries still
	// advance PlaylistEntry.Index, which stays the true playlist position.
	Skip func(id string) bool
	// Stop halts pagination at the first entry it matches (excluding that entry and
	// everything after) and leaves Playlist.Continuation empty. It is only correct
	// on an append-only newest-first feed such as a channel uploads playlist, where
	// a subscription poll stops at the first already-seen ID instead of paging the
	// whole feed. On a curated playlist, which can insert entries anywhere, Stop
	// drops items; use Skip there. Stop is checked before Skip.
	Stop func(id string) bool

	// OnProgress reports the running entry count after each playlist page. It is
	// optional and never triggers downloads.
	OnProgress func(items int)
	// OnEnrichProgress reports each completed InfoBasic refresh when Enrich is set.
	// Calls are serialized in increasing done-count order. The final call reaches
	// (total, total) unless context cancellation stops enrichment early.
	OnEnrichProgress func(done, total int)
}

EnumerateOptions tunes playlist enumeration. Enumeration never downloads.

type Event

type Event struct {
	Stage   Stage  // current pipeline stage
	VideoID string // empty for local-file processing

	// Downloading progress.
	Bytes int64
	Total int64 // 0 if unknown

	// CLI playlist expansion.
	ItemIndex int
	ItemCount int // total playlist entries, or 0 when unknown

	Warning *Warning // set when Stage == StageWarning
	Err     error    // set when Stage == StageFailed
	Message string   // optional human-readable detail
}

Event is a best-effort progress signal. Callbacks are invoked synchronously from the worker and are panic-recovered. A terminal event always fires: StageDone on success or StageFailed with Err. For Stream, the terminal event is emitted when the returned reader is closed.

type ExtractionError

type ExtractionError = waxerr.ExtractionError

Re-exported structured error types. Inspect them with errors.AsType, or with errors.As using a double-pointer target: each satisfies error on its pointer type (like *os.PathError), so the value form errors.As(err, &PlayabilityError{}) panics. Use:

var pe *PlayabilityError
if errors.As(err, &pe) { /* pe.Status, pe.Reason */ }

equivalently errors.AsType[*PlayabilityError](err).

type Format

type Format = format.Format

Audio format model and selectors (package format).

type HTTPStatusError

type HTTPStatusError = waxerr.HTTPStatusError

Re-exported structured error types. Inspect them with errors.AsType, or with errors.As using a double-pointer target: each satisfies error on its pointer type (like *os.PathError), so the value form errors.As(err, &PlayabilityError{}) panics. Use:

var pe *PlayabilityError
if errors.As(err, &pe) { /* pe.Status, pe.Reason */ }

equivalently errors.AsType[*PlayabilityError](err).

type InfoDepth

type InfoDepth uint8

InfoDepth selects how much work Info does. Callers do not pay for what they do not request.

const (
	// InfoBasic returns metadata and candidate formats (the default).
	InfoBasic InfoDepth = iota
	// InfoResolved additionally resolves the best-audio stream URL and expiry.
	// These signed googlevideo URLs are temporary and sensitive; the CLI omits
	// them from human output unless --show-url is given.
	InfoResolved
	// InfoProbe additionally runs ffprobe on the selected format only. This is
	// network-expensive (it reads the remote signed URL) and is never run on
	// every candidate.
	InfoProbe
)

type InfoResult

type InfoResult struct {
	Video  *Video // extracted metadata and candidate formats
	Client string // YouTube client that produced the metadata
	// SubstitutedFrom names a forced non-WEB client, such as WEB_EMBEDDED, that
	// the watch-page fallback replaced. When set, the metadata came from WEB
	// rather than the requested client.
	SubstitutedFrom string
	// ViaWatchPage reports that the primary metadata came from the watch-page
	// fallback. For a forced WEB client this read needs no PO token, unlike a forced
	// WEB stream; SubstitutedFrom stays empty because WEB is not substituted for
	// itself.
	ViaWatchPage bool
	// FullMetadata reports that watch-page enrichment (PublishDate, Chapters, and
	// Availability) was populated, either by the opt-in WithFullMetadata pass or
	// because the primary extraction already scraped the watch page (ViaWatchPage).
	// This is a different axis from ViaWatchPage, which reports where the base
	// metadata came from. When false, an empty Chapters slice or Unknown
	// Availability means enrichment did not run, not that the video has none.
	FullMetadata bool
	// Probed reports that InfoProbe ran ffprobe on the resolved best-audio stream,
	// so that row's sample rate, channels, bitrate, and duration are authoritative.
	// It is false for SABR streams, which have no direct URL to probe.
	Probed bool
	// BestIndex is the index into Video.Formats that InfoResolved/InfoProbe resolved
	// (and, for InfoProbe, probed in place). It is -1 at InfoBasic depth or when no
	// audio could be selected. A probe mutates that row, so callers should display
	// BestIndex rather than re-running selection on the mutated slice.
	BestIndex int
}

InfoResult contains extracted video metadata and the client that produced it. A later Resolve call may use a different client.

type LiveStatus

type LiveStatus = youtube.LiveStatus

LiveStatus reports a video's live-broadcast state. On a Video from Info it is LiveNone or LiveWasLive; live/upcoming videos surface as error sentinels.

type Locale

type Locale struct {
	HL string // host language, e.g. "en", "de", "ja"
	GL string // content region, e.g. "US", "DE", "JP"
}

Locale sets InnerTube localization hints. The zero value uses en / US.

HL affects localized UI text YouTube returns, including some error reasons. Availability classification of members-only and geo-blocked videos matches those reason strings and is tuned for English, so a non-English HL may report ErrVideoUnavailable instead of ErrMembersOnly or ErrGeoBlocked (both are still skip-class verdicts). GL is a content-region hint; it does not change the request IP or bypass geo restrictions. Titles and descriptions are usually returned as-authored.

type LoudnessInfo

type LoudnessInfo struct {
	IntegratedLUFS float64 // integrated loudness, LUFS
	TruePeakDBTP   float64 // true peak, dBTP
	LRA            float64 // loudness range, LU
	Threshold      float64 // relative gating threshold, LUFS
}

LoudnessInfo holds an EBU R128 measurement.

func (LoudnessInfo) MarshalJSON

func (l LoudnessInfo) MarshalJSON() ([]byte, error)

MarshalJSON encodes non-finite measurements as JSON null because encoding/json rejects NaN and Inf. Silent tracks can produce -Inf; using null keeps LoudnessInfo JSON-friendly for Measure and MeasureAlbum callers. Field names stay the exported struct names.

type LoudnessMode

type LoudnessMode uint8

LoudnessMode selects measurement vs. normalization.

const (
	// LoudnessMeasureOnly returns measurements without altering the audio.
	LoudnessMeasureOnly LoudnessMode = iota
	// LoudnessApply normalizes to Target, fused into the transcode pass. It
	// requires an encode, so it is rejected with FormatCopy or no transcode unless
	// an explicit output codec is given (ErrIncompatibleSpec).
	LoudnessApply
)

type LoudnessResult

type LoudnessResult struct {
	Input  *LoudnessInfo // measured input loudness (post-cut)
	Output *LoudnessInfo // post-apply loudness; set only when Mode == LoudnessApply
	Target float64       // requested integrated loudness in LUFS
}

LoudnessResult reports loudness measurements. WaxTap returns LUFS/true-peak measurements, not ReplayGain tag values.

type LoudnessSpec

type LoudnessSpec struct {
	// Mode selects measurement or applied normalization.
	Mode LoudnessMode
	// Target is the target integrated loudness in LUFS for Apply (e.g. -14). The
	// value is the caller's policy; WaxTap does not impose one.
	Target float64
}

LoudnessSpec requests loudness measurement or normalization (EBU R128).

type Options

type Options struct {
	// HTTPClient is used for all requests. It should set a DialContext and a
	// conservative Timeout, or rely on the per-operation context deadlines
	// WaxTap applies (see Timeouts). A no-timeout client on a dead proxy cannot
	// leak goroutines because WaxTap still bounds each operation by context. If
	// nil, a default client is used.
	HTTPClient *http.Client

	// Logger receives structured logs. If nil, logging is discarded; the CLI
	// installs its own handler.
	Logger *slog.Logger

	// Locale sets the InnerTube host language (hl) and content region (gl). The
	// zero value defaults to en / US.
	Locale Locale

	// CacheDir is the base directory for the on-disk player cache. Empty selects
	// os.UserCacheDir()/waxtap.
	CacheDir string
	// DisableDiskCache turns off on-disk player cache reads and writes.
	DisableDiskCache bool

	// TempDir is where intermediate/staging files are written; empty uses the OS
	// temp dir. MaxTempBytes optionally guards total staging size (0 =
	// unlimited). This is useful in constrained containers.
	TempDir string
	// MaxTempBytes limits temporary staging bytes. Zero disables the limit.
	MaxTempBytes int64

	Concurrency Concurrency // limits parallel network and ffmpeg work
	Timeouts    Timeouts    // sets per-operation deadlines
	Retry       RetryPolicy // tunes HTTP retries and backoff
	Politeness  Politeness  // limits request rate and applies cooldowns

	// ProfileOverridePath points at a strict JSON file that replaces the built-in
	// YouTube client profile chain at startup. Use it to refresh client versions,
	// user agents, or device fingerprints without rebuilding.
	ProfileOverridePath string

	// ChromeMajor overrides the emulated Chrome major for the built-in WEB-family
	// client identities. Zero selects the built-in default.
	//
	// The value applies to the default profile chain and to built-in WEB requests
	// used for discovery and fallbacks. It does not modify profiles loaded from
	// ProfileOverridePath, so the two options cannot be combined. New rejects
	// values outside 0..999.
	ChromeMajor int

	SponsorBlock SponsorBlockOptions // configures SponsorBlock API access

	// POTokenProvider supplies PO tokens for profiles that require them. WaxTap may
	// call it during extraction for a player-scope token and during resolution for
	// a GVS-scope stream token. When a download refresh follows a 403, WaxTap
	// passes the failure details in the request. Nil means no provider is
	// configured.
	POTokenProvider POTokenProvider

	// PlayerContextProvider enables the opt-in WEB SABR audio path: it supplies an
	// attested /player streaming context (from an external attesting browser such
	// as WaxSeal) that WaxTap streams Go-side. When set, WaxTap tries the WEB
	// context first (even over a forced Client, whose chain stays the fallback)
	// and falls back to the normal extraction chain on a context failure. It
	// needs a GVS PO-token provider too (the stream binds a GVS token to the
	// context's visitorData); New rejects a configuration without one, because
	// the token mint happens at SABR setup, past the fallback boundary. Nil
	// leaves WaxTap on its default chain.
	PlayerContextProvider PlayerContextProvider

	// Client, when non-empty, forces a single built-in client as the whole
	// strategy chain instead of the default multi-client fallback. Valid values
	// are "web", "ios", "android_vr", and "web_embedded". It applies the built-in
	// WEB-family User-Agent / ChromeMajor treatment. It is mutually exclusive with
	// ProfileOverridePath. A configured PlayerContextProvider is tried before
	// this chain; the forced client serves as its fallback.
	Client string

	// Session is an externally supplied guest identity (visitorData + cookies)
	// WaxTap adopts verbatim instead of bootstrapping its own, for byte-exact
	// session coherence with a PO-token minter. Session.VisitorData must be the
	// browser's exact X-Goog-Visitor-Id literal (the URL-escaped form in
	// ytcfg.VISITOR_DATA); it is re-sent with no escape/unescape.
	//
	// Adoption requires a uniform client chain (set Client, or a ProfileOverridePath
	// whose profiles are all one InnerTube client); the default multi-client chain
	// is rejected so an adopted session is never routed through a different client.
	// If resolution fails, extraction aborts rather than falling back to a random
	// synthetic visitorData. Adopted cookies need an HTTPClient with a cookie jar;
	// login cookies are dropped (adoption assumes a guest session). The adopted
	// session is resolved once per Client, so long-running services should recreate
	// the Client per task. Mutually exclusive with SessionProvider.
	Session *POTokenSession

	// SessionProvider resolves the adopted guest identity lazily, at most once per
	// Client (cached on success). It is the pull-based form of Session and shares
	// its uniform-chain requirement. Mutually exclusive with Session.
	SessionProvider POTokenSessionProvider
}

Options configures a Client. The zero value is usable; New fills in defaults for timeouts, retry policy, and limits. All fields are read once during New.

type Output

type Output struct {
	// contains filtered or unexported fields
}

Output is a delivery sink: either a file path or a writer. The zero value is unset. Construct it with ToFile or ToWriter.

The library writes the exact path given to ToFile (only a temp suffix and an atomic rename); filename templating, sanitization, and collision handling are the CLI's job, not the library's.

func ToFile

func ToFile(path string) Output

ToFile delivers to an exact file path (written atomically via a temp + rename).

func ToWriter

func ToWriter(w io.Writer) Output

ToWriter delivers to a caller-provided writer (bounded memory, no atomicity guarantee).

type POTokenFailure

type POTokenFailure = potoken.HTTPFailure

POTokenFailure describes the HTTP failure that triggered a token refresh. POTokenRequest.Failure points to a POTokenFailure when one is available.

type POTokenProvider

type POTokenProvider = potoken.Provider

PO-token provider contract (package potoken).

func NewSidecarPOTokenProvider

func NewSidecarPOTokenProvider(baseURL string, opts ...SidecarOption) (POTokenProvider, error)

NewSidecarPOTokenProvider returns a POTokenProvider that mints PO tokens from a bgutil-wire endpoint (the protocol bgutil-ytdlp-pot-provider and WaxSeal's token server speak). It POSTs a content_binding to <baseURL>/get_pot; baseURL may be a base such as "http://127.0.0.1:4416" or a full endpoint, and the default path is appended only when absent. A bad URL returns an error. Plug the result into Options.POTokenProvider.

The provider uses a dedicated 30s-timeout, no-redirect client that ignores Options.HTTPClient: a PO token is IP-bound, so the mint and the stream must share egress, and no-redirect pins credentials to the endpoint.

Example

ExampleNewSidecarPOTokenProvider wires a running WaxSeal sidecar into a client for full WEB SABR audio: a PO-token provider plus an attested player-context provider, both pointed at the same host so the token mint and the stream share egress. NewSidecarSessionProvider (adopted /session) is the alternative to the player-context handoff. See MAINTENANCE.md for the sidecar wire contracts.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/colespringer/waxtap/v2"
)

func main() {
	// The PO-token and player-context endpoints must share a host with the
	// download so the IP-bound token stays valid. Each accepts a base URL or a
	// full endpoint; add WithSidecarAPIKey for an authenticated sidecar.
	poToken, err := waxtap.NewSidecarPOTokenProvider("http://127.0.0.1:4416")
	if err != nil {
		log.Fatal(err)
	}
	playerContext, err := waxtap.NewSidecarPlayerContextProvider("http://127.0.0.1:4416")
	if err != nil {
		log.Fatal(err)
	}

	client, err := waxtap.New(waxtap.Options{
		POTokenProvider:       poToken,
		PlayerContextProvider: playerContext,
	})
	if err != nil {
		log.Fatal(err)
	}

	res, err := client.Download(context.Background(), waxtap.Request{
		URL:         "https://www.youtube.com/watch?v=VIDEO_ID_01",
		ProcessSpec: waxtap.ProcessSpec{Output: waxtap.ToFile("track.opus")},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("delivered %d bytes via %s\n", res.OutputBytes, res.Client)
}

type POTokenProviderFunc

type POTokenProviderFunc = potoken.ProviderFunc

POTokenProviderFunc adapts a closure to POTokenProvider.

type POTokenRequest

type POTokenRequest = potoken.Request

PO-token provider contract (package potoken).

type POTokenResponse

type POTokenResponse = potoken.Response

PO-token provider contract (package potoken).

type POTokenScope

type POTokenScope = potoken.Scope

PO-token provider contract (package potoken).

type POTokenSession

type POTokenSession = potoken.Session

External guest-session adoption (package potoken). A POTokenSession lets WaxTap adopt an externally supplied visitorData + cookies verbatim instead of bootstrapping its own, for byte-exact session coherence with a PO-token minter. POTokenSessionProvider is its pull-based form.

type POTokenSessionProvider

type POTokenSessionProvider = potoken.SessionProvider

External guest-session adoption (package potoken). A POTokenSession lets WaxTap adopt an externally supplied visitorData + cookies verbatim instead of bootstrapping its own, for byte-exact session coherence with a PO-token minter. POTokenSessionProvider is its pull-based form.

func NewSidecarSessionProvider

func NewSidecarSessionProvider(baseURL string, opts ...SidecarOption) (POTokenSessionProvider, error)

NewSidecarSessionProvider returns a POTokenSessionProvider that adopts a guest identity ({visitor_data, cookies}) from a <baseURL>/session endpoint (base or full endpoint accepted), so WaxTap streams under the same session the token was attested in. A bad URL returns an error. Plug the result into Options.SessionProvider.

The provider uses a dedicated 30s-timeout, no-redirect client that ignores Options.HTTPClient: full WEB validation requires the session host and the downloads to share an egress IP.

type PlayabilityError

type PlayabilityError = waxerr.PlayabilityError

Re-exported structured error types. Inspect them with errors.AsType, or with errors.As using a double-pointer target: each satisfies error on its pointer type (like *os.PathError), so the value form errors.As(err, &PlayabilityError{}) panics. Use:

var pe *PlayabilityError
if errors.As(err, &pe) { /* pe.Status, pe.Reason */ }

equivalently errors.AsType[*PlayabilityError](err).

type PlayerContext

type PlayerContext = potoken.PlayerContext

Attested WEB player-context handoff (package potoken). A PlayerContextProvider supplies an attested /player streaming context (serverAbrStreamingUrl, ustreamer config, visitorData, and audio formats) that WaxTap streams Go-side, enabling the opt-in WEB SABR audio path.

type PlayerContextFormat

type PlayerContextFormat = potoken.PlayerContextFormat

Attested WEB player-context handoff (package potoken). A PlayerContextProvider supplies an attested /player streaming context (serverAbrStreamingUrl, ustreamer config, visitorData, and audio formats) that WaxTap streams Go-side, enabling the opt-in WEB SABR audio path.

type PlayerContextProvider

type PlayerContextProvider = potoken.PlayerContextProvider

Attested WEB player-context handoff (package potoken). A PlayerContextProvider supplies an attested /player streaming context (serverAbrStreamingUrl, ustreamer config, visitorData, and audio formats) that WaxTap streams Go-side, enabling the opt-in WEB SABR audio path.

func NewSidecarPlayerContextProvider

func NewSidecarPlayerContextProvider(baseURL string, opts ...SidecarOption) (PlayerContextProvider, error)

NewSidecarPlayerContextProvider returns a PlayerContextProvider that fetches an attested WEB /player streaming context from a WaxSeal-style endpoint, enabling the opt-in WEB SABR audio path. It POSTs video_id to <baseURL>/player-context (base or full endpoint accepted). A bad URL returns an error. Plug the result into Options.PlayerContextProvider; New requires a POTokenProvider alongside it because the WEB stream binds a GVS PO token to the context's visitorData.

The client imposes no timeout of its own; calls rely on Timeouts.WebContext. Like the token provider it ignores Options.HTTPClient and is never proxied.

type PlayerContextProviderFunc

type PlayerContextProviderFunc = potoken.PlayerContextProviderFunc

Attested WEB player-context handoff (package potoken). A PlayerContextProvider supplies an attested /player streaming context (serverAbrStreamingUrl, ustreamer config, visitorData, and audio formats) that WaxTap streams Go-side, enabling the opt-in WEB SABR audio path.

type Playlist

type Playlist = youtube.Playlist

Extraction models (package youtube). Part of the volatile surface; may evolve pre-1.0.

type PlaylistDownloadOptions

type PlaylistDownloadOptions struct {
	// MaxItems limits playlist enumeration. Zero includes all entries.
	MaxItems int
	// Concurrency limits parallel downloads. Zero uses the client's download
	// concurrency, then a default of 2. BuildRequest calls remain serial and may
	// overlap a download even when Concurrency is 1.
	Concurrency int
	// MaxDownloads limits download attempts. Zero is unlimited. Skipped entries
	// and BuildRequest errors do not count.
	MaxDownloads int
	// SleepInterval is the minimum delay before each download start after the
	// first. Zero disables pacing. With Concurrency set to 1, the delay falls
	// between completed downloads; at higher concurrency, it spaces starts.
	SleepInterval time.Duration
	// MaxSleepInterval, when greater than SleepInterval, randomizes the delay
	// uniformly within that range. It requires a non-zero SleepInterval.
	MaxSleepInterval time.Duration

	// Skip and Stop drive an archive cursor during enumeration, matching
	// EnumerateOptions. Skip omits matching entries but keeps paging (safe on any
	// playlist); Stop halts paging at the first match (only correct on a newest-first
	// channel uploads feed). Using Stop for a subscription poll avoids paging the
	// whole feed each run instead of skipping only at BuildRequest time.
	Skip func(id string) bool
	Stop func(id string) bool

	// BuildRequest prepares a download request for each entry. Calls are serial
	// and follow playlist order. Returning a non-empty skip reason or an error
	// prevents the download and does not count toward MaxDownloads. Panics are
	// recovered and recorded as BuildRequest errors.
	BuildRequest func(ctx context.Context, e PlaylistEntry) (req Request, skip string, err error)
	// OnItem receives each attempted, skipped, or failed entry. It is not called
	// for entries left in Remaining. Calls may be concurrent and out of playlist
	// order. Panics are recovered and ignored.
	OnItem func(PlaylistItemOutcome)
}

PlaylistDownloadOptions configures Client.DownloadPlaylist. BuildRequest is required. Counts and durations must not be negative.

type PlaylistEntry

type PlaylistEntry = youtube.PlaylistEntry

Extraction models (package youtube). Part of the volatile surface; may evolve pre-1.0.

type PlaylistItemOutcome

type PlaylistItemOutcome struct {
	Entry      PlaylistEntry // playlist entry associated with this outcome
	Attempted  bool          // whether Download was called and counted against MaxDownloads
	Result     *Result       // set only after a successful download
	SkipReason string        // set when BuildRequest skipped the entry
	Err        error         // from BuildRequest when not Attempted, otherwise from Download
}

PlaylistItemOutcome describes the result for one playlist entry.

type PlaylistRunResult

type PlaylistRunResult struct {
	Enumerated         int                   // entries returned by playlist enumeration
	Downloaded         int                   // successful downloads
	Skipped            int                   // entries skipped by BuildRequest
	BuildRequestFailed int                   // entries whose BuildRequest call failed
	DownloadFailed     int                   // entries whose Download call failed
	Remaining          int                   // entries not reached because of a limit or cancellation
	CapReached         bool                  // whether MaxDownloads stopped the run
	EnumErrors         []error               // item errors returned by playlist enumeration
	Outcomes           []PlaylistItemOutcome // reached entries, in playlist order
}

PlaylistRunResult summarizes a playlist download.

Invariant: Downloaded + Skipped + BuildRequestFailed + DownloadFailed + Remaining equals Enumerated. MaxDownloads counts Downloaded and DownloadFailed.

type PlaylistUnavailableError

type PlaylistUnavailableError = waxerr.PlaylistUnavailableError

PlaylistUnavailableError reports why YouTube considers a playlist inaccessible.

type Politeness

type Politeness struct {
	// PerHostQPS throttles requests per host (0 = unlimited). youtube.com and
	// googlevideo.com are limited independently.
	PerHostQPS float64
	// Cooldown pauses requests to a host after HTTP 429, or after HTTP 503/403
	// with a Retry-After header. A longer Retry-After value takes precedence, up
	// to RetryPolicy.MaxRetryWait. Zero disables the cooldown.
	Cooldown time.Duration
}

Politeness governs request volume and backoff. The posture is to be a well-behaved client (reduce load, honor backoff, stop when limited), not to evade detection.

type ProcessRequest

type ProcessRequest struct {
	// Input is the local file path. Reader-based inputs will use a separate
	// request type; non-seekable inputs are staged before processing.
	Input string

	ProcessSpec
}

ProcessRequest processes a local audio file through the same pipeline as a YouTube download (transcode/cut/normalize), with no YouTube access.

type ProcessSpec

type ProcessSpec struct {
	Transcode *TranscodeSpec // nil = keep source, no re-encode
	Cut       *CutSpec       // nil = no cut
	Loudness  *LoudnessSpec  // nil = no loudness work

	// Channels is the Downmix target layout, applied after probing. When Downmix is
	// set it must be LayoutMono or LayoutStereo; pairing Downmix with LayoutAny is a
	// hard error. LayoutAny, the zero value, means no downmix target, so Downmix must
	// be false. For YouTube requests, prefer setting the layout on Audio with
	// WithChannels to pick a native track; audio selection already defaults to stereo,
	// so downmix is only needed when a caller opts into a surround source.
	Channels ChannelLayout
	// Downmix reduces a source with more channels to Channels after probing. It
	// never adds channels and does nothing when the source already fits the
	// requested layout. Channels must be LayoutMono or LayoutStereo. When
	// Transcode is nil, the encoder is chosen from the source codec and destination
	// container.
	Downmix bool

	// Output is the sink. For source-style delivery (an io.ReadCloser to pipe
	// elsewhere) use Client.Stream instead of setting Output.
	Output Output

	// Events receives best-effort, synchronous, panic-recovered stage events.
	// It may be nil. A slow callback backpressures the worker, so keep it fast.
	Events func(Event)

	// SkipIfExists skips work when the exact output path already exists. This is
	// only a path check; callers remain responsible for library-level deduping.
	SkipIfExists bool

	// IncludeMetadata attaches extended video metadata to Result.Metadata for
	// YouTube downloads. It has no effect on local-file processing.
	IncludeMetadata bool

	// Threads limits ffmpeg's worker threads for processing operations. Zero lets
	// ffmpeg choose.
	Threads int
}

ProcessSpec is the processing pipeline shared by YouTube and local-file requests. Each stage is opt-in: a nil pointer means that stage is skipped, so the default path keeps the selected source stream unchanged.

type ProviderError

type ProviderError = waxerr.ProviderError

ProviderError reports a failed player-context or session provider call.

type RateLimitError

type RateLimitError = waxerr.RateLimitError

Re-exported structured error types. Inspect them with errors.AsType, or with errors.As using a double-pointer target: each satisfies error on its pointer type (like *os.PathError), so the value form errors.As(err, &PlayabilityError{}) panics. Use:

var pe *PlayabilityError
if errors.As(err, &pe) { /* pe.Status, pe.Reason */ }

equivalently errors.AsType[*PlayabilityError](err).

type ReadOption

type ReadOption func(*readOptions)

ReadOption configures Info, InfoResult, and Resolve. WithNoFallback applies to all three; WithChannels only affects the best-audio row Info and InfoResult pick (Resolve takes an explicit AudioSelector and ignores it).

func WithChannels

func WithChannels(layout ChannelLayout) ReadOption

WithChannels sets the channel preference Info and InfoResult use to pick the best-audio row they resolve and probe, matching the row a default download would select. The facade defaults to stereo; pass WithChannels(LayoutSurround) for surround or WithChannels(LayoutAny) to rank purely by fidelity with no channel preference (a surround track may then rank highest). Resolve takes an explicit AudioSelector and ignores this option.

func WithFullMetadata

func WithFullMetadata() ReadOption

WithFullMetadata makes Info and InfoResult run a token-free watch-page pass that backfills PublishDate (when the primary client omitted it), Chapters, and Availability. The default ANDROID_VR client omits these, so this is what makes them reliable across clients. InfoResult.FullMetadata reports whether the data was populated.

It costs one extra HTTP request unless the primary extraction already scraped the watch page (InfoResult.ViaWatchPage), in which case the data is already present and no extra fetch runs. Enrichment is best-effort: a parse failure leaves the fields zero/Unknown rather than failing the call. WithFullMetadata is a no-op when combined with WithNoFallback, which forbids the watch page.

func WithNoFallback

func WithNoFallback() ReadOption

WithNoFallback prevents Info, InfoResult, and Resolve from falling back to watch-page extraction. Request.NoFallback provides the same behavior for Download and Stream.

type Request

type Request struct {
	// URL is a YouTube video URL or bare video ID.
	URL string

	// Audio selects which audio stream to take. The zero value is BestAudio, which
	// the facade defaults to stereo; use BestAudio().WithChannels(LayoutSurround) for
	// surround or WithChannels(LayoutAny) to rank purely by fidelity.
	Audio AudioSelector
	// SourcePolicy controls source selection when transcoding. The zero value is
	// MinimizeLoss.
	SourcePolicy SourcePolicy

	// NoFallback prevents fallback from a WEB player context to the configured
	// client chain, disables watch-page extraction, and prevents retrying another
	// client after an incomplete download. The configured extraction chain may
	// still select a working client. Set Options.Client to force a single client.
	// Read methods use WithNoFallback for the same behavior.
	NoFallback bool

	// FullMetadata runs a token-free watch-page pass during the download and fills
	// Result.Metadata with the PublishDate and Chapters that the default /player
	// client omits, so an ingest that needs them is one call instead of a separate
	// Info(..., WithFullMetadata()) plus Download. It requires IncludeMetadata and is
	// a no-op with NoFallback (which forbids the watch page). The extra fetch is
	// skipped when extraction already scraped the watch page. Enrichment is
	// best-effort: a failure leaves the base metadata and never fails the download.
	FullMetadata bool

	ProcessSpec
}

Request is a YouTube acquisition + processing request.

type RequestedFormatError

type RequestedFormatError = waxerr.RequestedFormatError

RequestedFormatError reports that an explicit itag/codec selector matched no available audio format and lists the available alternatives.

type ResolvedStream

type ResolvedStream = youtube.ResolvedStream

Extraction models (package youtube). Part of the volatile surface; may evolve pre-1.0.

type Result

type Result struct {
	SourceKind SourceKind // identifies a YouTube or local-file source
	VideoID    string     // empty for local files
	Title      string     // empty for local files
	InputPath  string     // set for local files
	OutputPath string     // empty for ToWriter delivery
	Client     string     // YouTube client used, such as "ANDROID_VR"; empty for local files

	SourceFormat Format // input/source format
	OutputFormat Format // after transcode (== source when copy/keep)

	SourceBytes int64 // bytes read from the acquired or local source
	OutputBytes int64 // bytes delivered to the output sink

	Transcoded          bool            // audio was re-encoded (not stream-copied); a copy/remux stays false
	CutApplied          bool            // at least one time range was removed
	SponsorBlockApplied bool            // SponsorBlock contributed a removed range
	LoudnessMeasured    bool            // measured != normalized
	LoudnessApplied     bool            // normalization was applied
	Loudness            *LoudnessResult // nil unless measured

	Warnings []Warning // non-fatal conditions encountered during processing

	// Metadata contains extended video metadata when ProcessSpec.IncludeMetadata
	// is set. It is nil otherwise.
	Metadata *VideoMetadata
}

Result reports the outcome of a Download or Process. Boolean flags describe completed effects, not requested work. For example, a SponsorBlock request that matches no segments leaves SponsorBlockApplied and CutApplied false.

type RetryPolicy

type RetryPolicy struct {
	MaxRetries  int           // additional attempts after the first
	BaseBackoff time.Duration // base of the exponential backoff
	MaxBackoff  time.Duration // cap on a single backoff sleep

	// MaxRetryWait caps an honored Retry-After. Beyond it WaxTap fails fast with
	// a *RateLimitError instead of sleeping a goroutine. Some Retry-After values
	// can be hours long.
	MaxRetryWait time.Duration
}

RetryPolicy tunes HTTP retry/backoff.

type Segment

type Segment = sponsorblock.Segment

Segment describes one SponsorBlock skip segment.

type SidecarError

type SidecarError struct {
	Label    string // provider name, such as "bgutil PO-token server"
	Endpoint string // configured endpoint (redacted in the Error string)
	Err      error  // underlying transport error; Unwrap returns it
}

SidecarError reports a connection failure to a configured sidecar endpoint. Its Error string self-redacts the endpoint; SidecarResponseError is the counterpart for a reachable endpoint that returned a non-OK status or unusable response.

func (*SidecarError) Error

func (e *SidecarError) Error() string

func (*SidecarError) Unwrap

func (e *SidecarError) Unwrap() error

type SidecarOption

type SidecarOption func(*sidecarConfig)

SidecarOption configures a sidecar provider built by NewSidecarPOTokenProvider, NewSidecarPlayerContextProvider, or NewSidecarSessionProvider.

func WithSidecarAPIKey

func WithSidecarAPIKey(key string) SidecarOption

WithSidecarAPIKey sends key as the X-API-Key header on every sidecar request. An empty key (the default) sends no header. Use HTTPS for a remote sidecar.

type SidecarResponseError

type SidecarResponseError struct {
	Label      string // provider name, such as "session endpoint"
	Endpoint   string // configured endpoint (redacted in the Error string)
	StatusCode int    // HTTP status, or 0 when a 200 carried invalid content
	Reason     string // short, sanitized reason; never raw response bytes
}

SidecarResponseError reports a non-OK status or an invalid response from a configured sidecar. StatusCode is zero when an HTTP 200 response had invalid content. SidecarError is reserved for connection failures. Its Error string self-redacts the endpoint.

func (*SidecarResponseError) Error

func (e *SidecarResponseError) Error() string

type SourceKind

type SourceKind uint8

SourceKind distinguishes a YouTube download from local-file processing.

const (
	SourceYouTube   SourceKind = iota // media acquired from YouTube
	SourceLocalFile                   // media read from a local file
)

func (SourceKind) String

func (k SourceKind) String() string

type SourcePolicy

type SourcePolicy = format.SourcePolicy

Audio format model and selectors (package format).

func BestNative

func BestNative() SourcePolicy

BestNative ignores target codec matching and uses normal best-audio ranking.

func MinimizeLoss

func MinimizeLoss() SourcePolicy

MinimizeLoss prefers a source in the target codec family, avoiding a cross-codec transcode when possible.

func PreferCodec

func PreferCodec(codec string) SourcePolicy

PreferCodec prefers a source in the named codec family when policy is active.

type SponsorBlockErrorPolicy

type SponsorBlockErrorPolicy uint8

SponsorBlockErrorPolicy governs SponsorBlock fetch failures only (ffmpeg cut/transcode failures are always hard errors).

const (
	// ProceedUncut logs a warning and delivers the full, uncut audio when the
	// SponsorBlock fetch fails or times out (the default).
	ProceedUncut SponsorBlockErrorPolicy = iota
	// FailDownload fails the whole request when the SponsorBlock fetch fails.
	FailDownload
)

type SponsorBlockOptions

type SponsorBlockOptions struct {
	// BaseURL overrides the SponsorBlock API base URL (empty = public default).
	BaseURL string
	// Timeout is a strict per-fetch timeout; if set it takes precedence over
	// Timeouts.SponsorBlock.
	Timeout time.Duration
}

SponsorBlockOptions configures the SponsorBlock client.

type Stage

type Stage uint8

Stage identifies a pipeline stage in an Event.

const (
	StageExtracting  Stage = iota // fetching and parsing source metadata
	StageResolving                // resolving the selected media stream
	StageDownloading              // transferring source bytes
	StageStaging                  // preparing a local working file
	StageProbing                  // inspecting media with ffprobe
	StageAnalyzing                // measuring loudness
	StageCutting                  // removing time ranges
	StageNormalizing              // applying loudness normalization
	StageTranscoding              // encoding or remuxing audio
	StageFinalizing               // delivering the completed output
	StageSkipped                  // skipping work because output already exists
	StageWarning                  // reporting a non-fatal warning
	StageDone                     // reporting successful completion
	StageFailed                   // reporting terminal failure
)

func (Stage) String

func (s Stage) String() string

type StreamInfo

type StreamInfo struct {
	VideoID       string // resolved YouTube video ID
	Title         string // extracted video title
	Format        Format // selected source format
	ContentLength int64  // 0 if unknown
	Client        string // YouTube client used, such as "ANDROID_VR"
}

StreamInfo is the initial metadata returned by Client.Stream alongside the stream reader. Final byte counts are known only after read-to-EOF/Close.

type Target

type Target = format.Target

Target describes a transcode output for source selection. The facade maps a TranscodeSpec onto it; most callers do not construct one directly.

type Thumbnail

type Thumbnail = youtube.Thumbnail

Extraction models (package youtube). Part of the volatile surface; may evolve pre-1.0.

type TimeRange

type TimeRange struct {
	Start time.Duration // inclusive start offset
	End   time.Duration // exclusive end offset
}

TimeRange is a half-open [Start, End) span. End must be greater than Start.

type Timeouts

type Timeouts struct {
	Extraction     time.Duration // player-response fetch + parse
	Resolve        time.Duration // stream-URL resolution (incl. cipher JS)
	WebContext     time.Duration // per attested /player-context fetch, mid-stream re-fetches included
	SponsorBlock   time.Duration // SponsorBlock fetch (see also SponsorBlock.Timeout)
	ChunkRetry     time.Duration // per-chunk deadline for ranged downloads
	FFmpegShutdown time.Duration // grace period before killing ffmpeg on cancel
}

Timeouts are per-operation deadlines applied through context. There is no single global download cap; each operation gets its own budget. A zero field means WaxTap adds no extra deadline for that operation.

type TranscodeFormat

type TranscodeFormat uint8

TranscodeFormat names an output preset. FormatCopy is the only no-re-encode path. FLAC, ALAC, and WAV preserve the decoded samples, but they are still decode-and-encode passes when the source is YouTube audio.

const (
	FormatCopy   TranscodeFormat = iota // remux / stream-copy (no re-encode)
	FormatFLAC                          // FLAC lossless audio
	FormatALAC                          // Apple Lossless audio
	FormatWAV                           // uncompressed PCM in a WAV container
	FormatMP3                           // MP3 audio
	FormatAAC                           // delivered in an .m4a container
	FormatOpus                          // Opus audio
	FormatVorbis                        // Vorbis audio
)

type TranscodeSpec

type TranscodeSpec struct {
	// Format selects the output preset.
	Format TranscodeFormat
	// Bitrate is the target bits per second for lossy presets (e.g. 256000).
	// Zero selects the preset default. Ignored by lossless presets.
	Bitrate int
}

TranscodeSpec requests ffmpeg processing. An explicit FormatCopy stream-copies through ffmpeg to remux into the destination container; a nil TranscodeSpec keeps the selected source bytes untouched.

type Tri

type Tri = format.Tri

Audio format model and selectors (package format).

type Video

type Video = youtube.Video

Extraction models (package youtube). Part of the volatile surface; may evolve pre-1.0.

type VideoMetadata

type VideoMetadata struct {
	Author      string        // channel / uploader name
	ChannelID   string        // YouTube channel ID (canonical UC identity anchor)
	Duration    time.Duration // video duration, 0 if unknown
	PublishDate time.Time     // publication date, zero if unknown
	Description string        // video description
	// Availability is the listing state. It is Public or Unlisted only when
	// Request.FullMetadata ran the watch-page pass that determines it; otherwise
	// AvailabilityUnknown.
	Availability Availability
	// Chapters are the video's chapter markers. They are populated only when
	// Request.FullMetadata ran the watch-page pass (the default /player response
	// omits them); nil otherwise.
	Chapters []Chapter
	Formats  []Format // full candidate audio (and incidental video) formats
}

VideoMetadata contains optional YouTube metadata that is not stored directly on Result.

type Warning

type Warning struct {
	Code   WarningCode // stable machine-readable identifier
	Detail string      // human-readable context
}

Warning is a typed, non-fatal signal. It is both delivered as a StageWarning Event and accumulated in Result.Warnings.

type WarningCode

type WarningCode uint8

WarningCode is a stable, machine-readable warning identifier. Warning.Detail is intended for people.

const (
	WarnProceedUncut        WarningCode = iota // SponsorBlock fetch failed; delivered uncut
	WarnFallbackProfile                        // a fallback client profile was used
	WarnURLReResolved                          // an expired stream URL was re-resolved
	WarnPlaylistEntryFailed                    // one playlist entry failed (others returned)
	WarnRateLimitedRetried                     // a request was retried after a 429
	WarnSponsorBlockEmpty                      // SponsorBlock matched no segments
	WarnRangesEmpty                            // SponsorBlock segments all fell outside the media
	WarnThrottled                              // a limiter/cooldown is active
	WarnWebContextFallback                     // WEB player-context failed; fell back to the configured chain
	WarnIncompleteFallback                     // a client returned an incomplete stream; switched clients
	WarnWebContextRetry                        // WEB player-context was capped (status 2); retried once with a fresh context
)

func (WarningCode) String

func (w WarningCode) String() string

Directories

Path Synopsis
cmd
waxtap command
Command waxtap provides the WaxTap CLI for YouTube audio downloads and local audio processing.
Command waxtap provides the WaxTap CLI for YouTube audio downloads and local audio processing.
Package cut removes time ranges from an audio file.
Package cut removes time ranges from an audio file.
Package download transfers resolved media streams to files, writers, or callers that want an io.ReadCloser.
Package download transfers resolved media streams to files, writers, or callers that want an io.ReadCloser.
Package format defines WaxTap's stream-format model and the rules for picking an audio source from a candidate list.
Package format defines WaxTap's stream-format model and the rules for picking an audio source from a candidate list.
internal
cache
Package cache provides a thread-safe LRU cache with TTL expiry, schema versioning, and singleflight de-duplication of concurrent loads.
Package cache provides a thread-safe LRU cache with TTL expiry, schema versioning, and singleflight de-duplication of concurrent loads.
clientident
Package clientident defines the built-in browser identity used by WaxTap's WEB-family YouTube clients.
Package clientident defines the built-in browser identity used by WaxTap's WEB-family YouTube clients.
diskcache
Package diskcache provides a small on-disk blob cache for data that is expensive to fetch and safe to lose.
Package diskcache provides a small on-disk blob cache for data that is expensive to fetch and safe to lose.
dumpfile
Package dumpfile writes timestamped diagnostic artifacts.
Package dumpfile writes timestamped diagnostic artifacts.
httpx
Package httpx is WaxTap's internal HTTP client wrapper.
Package httpx is WaxTap's internal HTTP client wrapper.
iox
Package iox holds small io helpers shared across WaxTap.
Package iox holds small io helpers shared across WaxTap.
pipeline
Package pipeline runs WaxTap's source-agnostic audio processing on a staged local file: it cuts time ranges, normalizes loudness, and transcodes, fusing whatever is requested into a single ffmpeg encode.
Package pipeline runs WaxTap's source-agnostic audio processing on a staged local file: it cuts time ranges, normalizes loudness, and transcodes, fusing whatever is requested into a single ffmpeg encode.
tempfile
Package tempfile stages output in the destination directory and publishes it with an atomic rename.
Package tempfile stages output in the destination directory and publishes it with an atomic rename.
Package normalize measures EBU R128 loudness with ffmpeg's loudnorm filter and builds the matching apply filter for a later encode.
Package normalize measures EBU R128 loudness with ffmpeg's loudnorm filter and builds the matching apply filter for a later encode.
Package potoken defines the PO-token provider contract and the related browser-attested handoff contracts (Session and PlayerContext) that let WaxTap adopt an external attesting browser's identity and streaming context.
Package potoken defines the PO-token provider contract and the related browser-attested handoff contracts (Session and PlayerContext) that let WaxTap adopt an external attesting browser's identity and streaming context.
Package sponsorblock defines the SponsorBlock category vocabulary used by WaxTap cut requests.
Package sponsorblock defines the SponsorBlock category vocabulary used by WaxTap cut requests.
Package transcode wraps ffmpeg and ffprobe for local audio files.
Package transcode wraps ffmpeg and ffprobe for local audio files.
Package waxerr defines the sentinel and structured errors shared by WaxTap's extraction, download, and processing packages.
Package waxerr defines the sentinel and structured errors shared by WaxTap's extraction, download, and processing packages.
Package youtube performs YouTube extraction: it turns a URL into video metadata and candidate audio formats, and resolves those formats into playable, signed stream URLs.
Package youtube performs YouTube extraction: it turns a URL into video metadata and candidate audio formats, and resolves those formats into playable, signed stream URLs.
internal/resolver
Package resolver isolates YouTube's volatile player JavaScript.
Package resolver isolates YouTube's volatile player JavaScript.
internal/sabr
Package sabr streams YouTube SABR audio over UMP.
Package sabr streams YouTube SABR audio over UMP.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL