> built 2026-10-14 12:40 UTC from 9062594 (main) · yb 0.1.19. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# yb

Streamline media publishing: **prepare a piece of media once, publish it
anywhere** — to YouTube (upload + edit) or as a podcast (show notes, chapters,
RSS) — plus a download module for pulling videos back down.

```python
import yb

# Download a video (-> ~/Downloads, named "Title (id).mp4")
r = yb.download_youtube_video("https://youtu.be/PRa9ciOe-us")

# One call: transcribe, write title/description/keywords, detect chapters,
# render a thumbnail, and upload to YouTube (unlisted) with captions.
yb.prepare_and_publish(
    r.path,
    language="English",
    language_code="en",
    audio_language_code="en",
    privacy_status="unlisted",
)
```

## Why

`yb` separates **what** you publish from **where** you publish it. A media file
is turned into a platform-neutral [`PublicationContent`]() — title,
description, keywords, chapters, captions, thumbnail — and thin *adapters* map
that onto each destination’s API. The media work itself (transcription, chapter
detection, thumbnails, dubbing/translation) lives in the
[`mixing`](https://github.com/thorwhalen/mixing) package; `yb` is the
publication layer on top.

## Install

```bash
pip install yb                 # core (needs `mixing`)
pip install 'yb[youtube]'      # YouTube upload/edit (Google API client)
pip install 'yb[download]'     # yt-dlp downloader
pip install 'yb[podcast]'      # podcast chapters (ID3/PSC) + RSS
pip install 'yb[llm]'          # LLM metadata/chapter copy (via `aix`)
pip install 'yb[all]'          # everything
```

Everything heavy is an optional extra, and `import yb` never fails for a missing
one — the error only surfaces when you actually call that feature. You also need
**ffmpeg** on PATH for media work, and an `ELEVENLABS_API_KEY` for transcription.

### System dependencies

- **ffmpeg** on PATH, for media work.
- **Deno**, for downloading from YouTube. YouTube makes yt-dlp solve a JavaScript challenge; without a runtime, downloads fail with `No video formats found!`. `yb[download]` depends on `yt-dlp[default]`, which brings the solver scripts; the runtime you install yourself: `curl -fsSL https://deno.land/install.sh | sh` (or `brew install deno`, or `pip install 'yt-dlp[deno]'` into the same environment). Deno is the runtime yt-dlp enables by default; to use another (node, bun), enable it yourself with `extra_opts={'js_runtimes': {'node': {}}}`. The installer puts Deno in `~/.deno/bin` and adds it to PATH only for interactive shells; yb also looks there (and next to the running Python), and passes yt-dlp the path, so cron jobs and services work too. `yb check` also runs `deno --version` and reports a Deno older than yt-dlp’s minimum (read from yt-dlp itself) with the `deno upgrade` command. To put it on PATH anyway: `export PATH="$HOME/.deno/bin:$PATH"` in `~/.profile`. See yt-dlp’s [EJS notes](https://github.com/yt-dlp/yt-dlp/wiki/EJS).

Run `yb check` (or `yb.download.check_requirements()`) to see what is missing and how to install it. The first download in a process runs the same check and warns; it never refuses to try.

## Common tasks

### Download

```python
from yb.download import (
    download_youtube_video,
    download_youtube_audio,
    youtube_video_info,
)

youtube_video_info(url)  # metadata only, no download
download_youtube_video(url)  # best quality -> ~/Downloads/"Title (id).mp4"
download_youtube_video(
    url,
    download_dir="~/clips",
    write_info_json=True,
    write_subtitles=True,
    subtitle_langs=("en", "fr"),
)

download_youtube_audio(url)  # audio only, source format -> "Title (id).webm"
download_youtube_audio(url, audio_format="mp3")  # ...converted (needs ffmpeg)
download_youtube_audio(url, audio_format="mp3", bitrate="320k")
```

The destination defaults to `$YB_DOWNLOAD_DIR` (else `~/Downloads`). Every knob
— format, filename template, which sidecar metadata to also save — is
overridable, and any raw yt-dlp option can be passed via `extra_opts=`.

**Cookies (headless servers).** YouTube bot-blocks anonymous downloads from many
servers. Export a Netscape-format cookies file from a logged-in browser elsewhere
and point yb at it with `YB_COOKIES_FILE=/path/cookies.txt` (or just put it at
`~/.config/yb/youtube-cookies.txt`); every download and info call then uses it,
with no extra argument. Pass `cookies_file=` to choose one per call (`False`
turns the default off). An explicit `cookies_from_browser=` or a `cookiefile` /
`cookiesfrombrowser` in `extra_opts=` always wins. With no file and no env var,
nothing changes. Never commit the file.

**Audio format.** `download_youtube_audio` keeps whatever the source offers by
default (YouTube’s best audio is normally Opus in a `.webm` container). That
default needs no ffmpeg and avoids re-encoding one lossy format into another.
Pass `audio_format=` when you need a specific one — it’s a no-op if the download
already is that format, so it’s safe to pass unconditionally. If conversion
can’t happen (no ffmpeg) it raises, and **the download is always left on disk**;
pass `on_error="warn"` to fall back to the unconverted file instead.

**A time range.** To source a sound or a clip without downloading the whole video, pass `start=` and/or `end=` to `download_youtube_video` or `download_youtube_audio` (seconds, or `"mm:ss"` / `"hh:mm:ss"`):

```python
clip = download_youtube_audio(url, start="0:07.9", end="0:08.75")
clip.info["download_range"]  # {"start": 7.9, "end": 8.75}, for provenance
```

The range is cut by ffmpeg (so it needs one; to use a specific build pass `extra_opts={"ffmpeg_location": "/path/to/ffmpeg"}`, e.g. when the system ffmpeg lacks what yt-dlp asks of it). Cuts land on keyframes; `precise_cuts=True` re-encodes for frame-accurate bounds. With the default filename template the range goes in the name (`Title (id) [7.9-8.75].webm`), so a clip never collides with the full download (a `precise_cuts` clip is named apart from a keyframe cut too).

The same conversion is available standalone for files you already have:

```python
from yb.audio_convert import convert_audio

convert_audio("talk.webm", "mp3")  # -> talk.mp3
convert_audio("talk.webm", "wav")  # lossless: bitrate not applied
```

### Publish to YouTube

```python
from yb.youtube import prepare_and_publish, VideoMetadata, publish_video, CaptionTrack

# High level: prepare assets from the media file, then upload.
result = prepare_and_publish(
    "promo.fr.mp4",
    language="French",
    language_code="fr",
    audio_language_code="fr",
    privacy_status="unlisted",
)
print(result["url"])

# Lower level: you supply the metadata and captions.
meta = VideoMetadata(
    title="...",
    description="...",
    tags=[...],
    default_language="en",
    default_audio_language="en",
)
publish_video(
    "promo.en.mp4",
    meta,
    privacy_status="unlisted",
    captions=[CaptionTrack("promo.en.srt", "en", "English")],
    thumbnail="thumb.jpg",
)
```

If captions, the thumbnail or the playlist fail after the upload (a 403 `quotaExceeded`, say), `PartialPublish` is raised with the new video’s id in `err.result`. Call again with `resume=err.result` to finish without uploading a second copy.

`PartialPublish` is a `RuntimeError` and replaces the original error, which stays reachable as `err.__cause__`. Code that caught `googleapiclient.errors.HttpError` around a publish call no longer catches a failure that happens after the upload: catch `PartialPublish` too (or inspect `err.__cause__`).

**Chapters** are on by default: when the media is long enough to host them
(YouTube requires ≥3 markers, each ≥10 s, first at `0:00`), they are detected
from the transcript and embedded in the description so YouTube renders
interactive chapters. Short clips simply get none.

### Edit an already-published video

```python
from yb.youtube import update_video_fields, upsert_caption, set_chapters, set_thumbnail
from mixing.chapters import Chapter

update_video_fields("VIDEO_ID", title="New title", tags=["a", "b"])
upsert_caption(
    "VIDEO_ID", "subs.fr.srt", language="fr", name="Français"
)  # replaces same-lang track
set_chapters("VIDEO_ID", [Chapter(0, "Intro"), Chapter(45, "Demo")])
set_thumbnail("VIDEO_ID", "thumb.jpg")
```

Privacy, and replacing a published video with a re-cut (YouTube cannot swap the file):

```python
from yb.youtube import update_video_status, supersede

update_video_status("VIDEO_ID", privacy_status="public")  # other status fields kept
supersede(
    "OLD_ID", "NEW_ID", new_privacy="public"
)  # new takes old's playlist spots; old goes private
```

`supersede` does not change the new video’s privacy unless you pass `new_privacy=`, and uploads are `unlisted` by default. So superseding a **public** video with a fresh upload leaves the public with neither: the old one goes private and the re-cut stays unlisted. When that happens, `supersede` emits a `VisibilityDropWarning` before it changes anything. Pass `new_privacy="public"` to publish the re-cut, or make the warning an error to refuse such calls:

```python
import warnings
from yb.youtube import VisibilityDropWarning

warnings.simplefilter(
    "error", VisibilityDropWarning
)  # supersede now raises, changing nothing
```

### Read live stats & metadata

```python
from yb.youtube import video_metadata, FIELD_GROUPS

video_metadata(
    "VIDEO_ID", group="engagement"
)  # dict: views, likes, dislikes, comments, ...
print(
    video_metadata("VIDEO_ID", group="engagement", as_table=True)
)  # readable ASCII table
video_metadata(
    "VIDEO_ID", fields=["title", "views", "likes"]
)  # pick & order exact fields
print(
    video_metadata(["ID1", "ID2"], group="engagement", as_table=True)
)  # compare videos, one row each
```

One request returns a flat, typed view of a video’s live numbers plus content
and status details. With no `group`/`fields` you get every field; `fields`
overrides `group`. Named groups (`FIELD_GROUPS`): `engagement`, `overview`,
`content`, `status`, `identity`. `dislikes` is returned only to the video’s
**owner** (else `None`); watch-time/retention/shares live in the YouTube
*Analytics* API, not here.

### Publish a song as a music video

You have a song and its cover; YouTube wants a video. `yb.music` renders one
(through [`muvid.visualize`](https://github.com/thorwhalen/muvid)) and publishes
it through the same machinery as everything else. Needs `pip install 'yb[music]'`
(pulls `muvid`, which needs `ffmpeg`); uploading also needs `yb[youtube]`.

```python
from yb.music import prepare_music_video, publish_music, publish_folder

# Simplest: the cover on a 16:9 canvas, held for the song's length.
mv = prepare_music_video("song.wav", image="cover.png")
mv.video, mv.thumbnail  # -> song.mp4, song.thumb.jpg

# Render and upload in one call.
publish_music("song.wav", image="cover.png", privacy_status="unlisted")
```

**A whole folder → an album.** Drop `(song.wav, song.jpeg)` pairs in a folder —
the **filename is the title**, covers match by name — and publish them all, each
song rendered with the next visual in a rotation:

```python
# Test the first song (unlisted) before committing the set…
publish_folder("~/album", limit=1)
# …then do the rest, collected into one playlist.
publish_folder("~/album", playlist="My Album")
```

By default the rotation is `waves → cqt → spectrum → bars → scope` (override with
`cycle=`), so a set varies method to method yet reads as one release — reinforced
by a consistent **teal** accent and one loudness target across every video.
`prepare_folder(...)` renders without uploading, for review first.

Pick a single **visual** with `visual=`: `"still"` (default when there’s an
image), `"ken_burns"` (a slow pan/zoom), or an audio-reactive one — `"cqt"`
(pitch-aligned bars, the most musical), `"bars"`, `"spectrum"`, `"waves"`,
`"scope"`. All the reactive ones are pure ffmpeg. You can also pass **your own
callable** (see `muvid.visualize.register_visual`).

What you get for free, because it’s what makes a set of songs feel like a release:

- **16:9, never pillarboxed.** Square or portrait art is composed onto a 1080p
  canvas filled with a blurred, darkened copy of itself — not black bars.
- **Loudness-normalized** to −14 LUFS (EBU R128, two-pass), so an album plays at
  one level. `normalize=False` to opt out.
- **A thumbnail** derived from the same composition (1280×720, under YouTube’s
  2 MiB limit), used as the video’s thumbnail by default.
- **Music category**, and a video exactly as long as the song.

Check what you made before you ship it:

```python
from muvid.visualize import verify_video, report

print(report(verify_video("song.mp4", audio="song.wav", thumbnail="song.thumb.jpg")))
```

See the **`music2video`** skill for the visualizer menu and the full quality
checklist.

### Publish a podcast episode

```python
from yb.podcast import (
    prepare_podcast_episode,
    PodcastChannel,
    EpisodeFeedItem,
    build_feed,
)

ep = prepare_podcast_episode(
    "episode.mp3", "out/", cover_image="cover.jpg", make_cover_video=True
)
# -> out/episode.mp3 (with ID3 chapters), .shownotes.txt, .psc.xml,
#    .chapters.json, and .cover.mp4

# Assemble an RSS feed (delivery to Spotify/Apple is via your podcast host).
```

A podcast episode and a YouTube video are built from the *same*
`PublicationContent`: an episode “could just be audio with a cover image” (or a
Ken Burns video), which is exactly what `prepare_podcast_episode` can render.

## Setup (YouTube OAuth)

YouTube upload needs a Google OAuth *Desktop* client (one-time). `gcloud` is
**not required** — the console path works. See the **`yb-setup`** skill for the
full walkthrough (enable the API, create the OAuth client, run consent, test).
Point `yb` at the client JSON via `$YOUTUBE_CLIENT_SECRETS_FILE`.

Publish the OAuth consent screen (Audience → *Publish app*) before consenting:
tokens minted while it is in *Testing* expire after 7 days. With no browser or
terminal on the machine (a phone-driven session), consent by paste-back:
`yb auth`, open the URL anywhere, then `yb auth --paste '<redirected URL>'`.

> Note: until your Google Cloud project passes YouTube’s one-time API
> compliance audit, uploads may be forced to **private** even when you request
> `unlisted`/`public`.

## Layout

| Module             | Role                                                          | Extra                                |
|--------------------|---------------------------------------------------------------|--------------------------------------|
| `yb.content`       | platform-neutral content prep (metadata, chapters, thumbnail) | core                                 |
| `yb.music`         | songs → music videos, published as an album                   | `music` (+`youtube` to upload)       |
| `yb.youtube`       | upload, edit + read stats via YouTube Data API v3             | `youtube`                            |
| `yb.podcast`       | show notes, ID3/PSC chapters, cover video, RSS                | `podcast` (+`music` for cover video) |
| `yb.download`      | yt-dlp downloader (video, audio-only, playlists, metadata)    | `download`                           |
| `yb.audio_convert` | audio format conversion via ffmpeg                            | core (needs `ffmpeg`)                |

The audio→video rendering itself lives in
[`muvid.visualize`](https://github.com/thorwhalen/muvid) (canvas, visual
strategies, loudness, verification); `yb.music` is the publish-facing facade over
it, and the `music` extra pulls `muvid`.

<p class="epythet-aggregates">This documentation as a single file: <a href="yb.md">yb.md</a> (Markdown, for agents).</p>


# _autosummary/yb.audio_convert.html.md

# yb.audio_convert

Convert audio files between formats with ffmpeg.

Audio arrives in whatever format the source offers — YouTube’s best audio is
typically Opus in a `.webm` container — which is usually fine to keep as is.
Publishing paths, though, often want something specific: `.mp3` for podcast
players, `.wav` for editing. [`convert_audio()`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.convert_audio) is the one-call bridge:

```pycon
>>> from yb.audio_convert import convert_audio
>>> convert_audio("talk.webm", "mp3")
PosixPath('talk.mp3')
```

Conversion is a **no-op when the source is already in the target format**, so it
is safe to call unconditionally:

```pycon
>>> convert_audio("talk.mp3", "mp3")
PosixPath('talk.mp3')
```

Requires `ffmpeg` on `PATH`; without it [`AudioConversionError`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.AudioConversionError) is
raised (see also the `on_error="warn"` escape hatch in
[`yb.download.download_youtube_audio()`](_autosummary/yb.download.html.md#yb.download.download_youtube_audio)).

### Module Attributes

| [`LOSSLESS_FORMATS`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.LOSSLESS_FORMATS)      | Formats that store samples losslessly — a target bitrate is meaningless there.   |
|------------------------------------------------------------------------|----------------------------------------------------------------------------------|
| [`DEFAULT_LOSSY_BITRATE`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.DEFAULT_LOSSY_BITRATE) | Bitrate applied to lossy targets when the caller doesn't specify one.            |

### Functions

| [`convert_audio`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.convert_audio)(src, audio_format, \*[, ...])   | Convert `src` to `audio_format`, returning the resulting path.   |
|------------------------------------------------------------------------------------------------|------------------------------------------------------------------|
| [`normalize_format`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.normalize_format)(audio_format)                | Normalize a format spec to a bare lowercase extension.           |

### Exceptions

| [`AudioConversionError`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.AudioConversionError)   | Raised when audio could not be converted to the requested format.   |
|-------------------------------------------------------------------------|---------------------------------------------------------------------|

### *exception* yb.audio_convert.AudioConversionError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when audio could not be converted to the requested format.

### yb.audio_convert.DEFAULT_LOSSY_BITRATE *= '192k'*

Bitrate applied to lossy targets when the caller doesn’t specify one.

### yb.audio_convert.LOSSLESS_FORMATS *= frozenset({'aif', 'aiff', 'alac', 'flac', 'wav'})*

Formats that store samples losslessly — a target bitrate is meaningless there.

### yb.audio_convert.convert_audio(src, audio_format, , output=None, bitrate=None, overwrite=True, extra_ffmpeg_args=())

Convert `src` to `audio_format`, returning the resulting path.

* **Parameters:**
  * **src** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – The audio file to convert.
  * **audio_format** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Target format as an extension (`"mp3"`, `".wav"`, …).
  * **output** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Destination path. Defaults to `src` with the new extension.
  * **bitrate** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Target bitrate for lossy formats (e.g. `"320k"`). Defaults to
    [`DEFAULT_LOSSY_BITRATE`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.DEFAULT_LOSSY_BITRATE); ignored for [`LOSSLESS_FORMATS`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.LOSSLESS_FORMATS).
  * **overwrite** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Overwrite `output` if it already exists.
  * **extra_ffmpeg_args** ([`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Raw ffmpeg arguments, appended last so they win.
* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
* **Returns:**
  The converted file’s path — or `src` unchanged when it is already in
  the target format (no pointless re-encode, no generation loss).
* **Raises:**
  * [**FileNotFoundError**](https://docs.python.org/3/builtins/exceptions.html#FileNotFoundError) – If `src` does not exist.
  * [**AudioConversionError**](_autosummary/yb.audio_convert.html.md#yb.audio_convert.AudioConversionError) – If ffmpeg is missing, or the conversion fails.

### yb.audio_convert.normalize_format(audio_format)

Normalize a format spec to a bare lowercase extension.

Accepts it with or without the leading dot, in any case:

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> normalize_format("mp3"), normalize_format(".MP3"), normalize_format(" .Wav ")
('mp3', 'mp3', 'wav')
```


# _autosummary/yb.config.html.md

# yb.config

User configuration for `yb` publishing defaults.

Single source of truth for the publishing defaults you don’t want to repeat on
every call — the privacy a new upload gets, and a playlist to drop every upload
into so you can find your videos later. The config lives next to the OAuth token
(`$XDG_CONFIG_HOME/yb/config.json` or `~/.config/yb/config.json`) so all of
`yb`’s persistent state sits in one directory.

The file is **optional**: with no file present the built-in defaults apply
(`privacy_status="unlisted"`, no playlist). Any subset of keys may be set;
unspecified keys fall back to the built-in defaults, and unknown keys are
ignored (forward-compatible).

Example `~/.config/yb/config.json`:

```default
{
    "privacy_status": "unlisted",
    "playlist": "TW Uploads",
    "create_playlist_if_missing": true,
    "playlist_privacy_status": "private"
}
```

### Module Attributes

| [`DEFAULT_PRIVACY_STATUS`](_autosummary/yb.config.html.md#yb.config.DEFAULT_PRIVACY_STATUS)          | Default privacy for a freshly uploaded video when neither the call nor the config file specifies one.   |
|----------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
| [`DEFAULT_PLAYLIST_PRIVACY_STATUS`](_autosummary/yb.config.html.md#yb.config.DEFAULT_PLAYLIST_PRIVACY_STATUS) | Default privacy for a playlist that `yb` auto-creates.                                                  |

### Functions

| [`default_config_file`](_autosummary/yb.config.html.md#yb.config.default_config_file)()      | Config file location (`$XDG_CONFIG_HOME` or `~/.config`).              |
|-----------------------------------------------------------------------------|------------------------------------------------------------------------|
| [`load_config`](_autosummary/yb.config.html.md#yb.config.load_config)([config_file]) | Load publishing defaults from `config_file` (or the default location). |

### Classes

| [`YbConfig`](_autosummary/yb.config.html.md#yb.config.YbConfig)([privacy_status, playlist, ...])   | Resolved publishing defaults.   |
|----------------------------------------------------------------------------------------------|---------------------------------|

### yb.config.DEFAULT_PLAYLIST_PRIVACY_STATUS *= 'private'*

Default privacy for a playlist that `yb` auto-creates. `"private"` means
only you see it — ideal for a personal “find my uploads” playlist.

### yb.config.DEFAULT_PRIVACY_STATUS *= 'unlisted'*

Default privacy for a freshly uploaded video when neither the call nor the
config file specifies one. `"unlisted"` keeps videos off your public feed
but shareable by link until you deliberately make one `"public"`.

### *class* yb.config.YbConfig(privacy_status='unlisted', playlist=None, create_playlist_if_missing=True, playlist_privacy_status='private')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Resolved publishing defaults.

#### privacy_status

Privacy a new upload gets (`unlisted` | `private` |
`public`) when the call doesn’t override it.

#### playlist

Title of a playlist every upload is added to (`None` = none).

#### create_playlist_if_missing

Create `playlist` if no playlist of that
title exists yet, rather than erroring.

#### playlist_privacy_status

Privacy for an auto-created playlist.

#### *classmethod* from_mapping(mapping)

Build a config from a mapping, ignoring unknown keys.

* **Return type:**
  [`YbConfig`](_autosummary/yb.config.html.md#yb.config.YbConfig)

### yb.config.default_config_file()

Config file location (`$XDG_CONFIG_HOME` or `~/.config`).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### yb.config.load_config(config_file=None, \*\*overrides)

Load publishing defaults from `config_file` (or the default location).

A missing file yields the built-in defaults. `overrides` (typically the
explicit keyword arguments a caller passed) take precedence when not
`None`, so call-site arguments always win over the file.

* **Return type:**
  [`YbConfig`](_autosummary/yb.config.html.md#yb.config.YbConfig)


# _autosummary/yb.content.html.md

# yb.content

Target-agnostic publication content: prepare it once, publish anywhere.

A [`PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent) bundles everything a publication needs that is
*not* specific to a destination — title, description, keywords, chapters,
languages, and the media/transcript/thumbnail assets. Platform adapters
(`yb.youtube`, `yb.podcast`) consume it and map it to their own schema.

The heavy lifting (transcription, chapter detection, thumbnail rendering) is
delegated to the `mixing` package; the copywriting (title/description/
keywords) is LLM-backed via `aix` and pluggable.

### Functions

| [`format_chapter_lines`](_autosummary/yb.content.html.md#yb.content.format_chapter_lines)(chapters)                     | Render chapters as `M:SS Title` lines (the shared text convention).                                             |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| [`generate_metadata`](_autosummary/yb.content.html.md#yb.content.generate_metadata)(transcript, \*[, language, ...]) | Generate platform-neutral title/description/keywords from a transcript.                                         |
| [`prepare_content`](_autosummary/yb.content.html.md#yb.content.prepare_content)(media, \*[, language, ...])        | Build a [`PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent) for `media` (destination-agnostic). |

### Classes

| [`ContentMetadata`](_autosummary/yb.content.html.md#yb.content.ContentMetadata)(title, description[, keywords])   | Platform-neutral copy: a title, a description, and keywords.         |
|----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent)(media[, title, ...])           | Everything needed to publish a piece of media, destination-agnostic. |

### *class* yb.content.ContentMetadata(title, description, keywords=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Platform-neutral copy: a title, a description, and keywords.

### *class* yb.content.PublicationContent(media, title='', description='', keywords=<factory>, chapters=<factory>, language=None, audio_language=None, srt_path=None, thumbnail=None, duration=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Everything needed to publish a piece of media, destination-agnostic.

#### media

Path to the audio/video file.

#### title

Headline/title.

#### description

Long-form description / show notes body.

#### keywords

Topical keywords/tags.

#### chapters

Ordered chapter markers (`mixing.chapters.Chapter`).

#### language

BCP-47 code of the metadata text (e.g. `"en"`).

#### audio_language

BCP-47 code of the spoken audio (e.g. `"fr"`).

#### srt_path

Path to the SRT subtitle/caption file, if any.

#### thumbnail

Path to a thumbnail/cover image, if any.

#### duration

Media duration in seconds, if known.

#### description_with_chapters()

Description with a chapters block appended (if any chapters).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.content.format_chapter_lines(chapters)

Render chapters as `M:SS Title` lines (the shared text convention).

Uses `H:MM:SS` when any chapter is at or beyond one hour. This is the
text format both YouTube descriptions and podcast show notes accept.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.content.generate_metadata(transcript, , language='English', brand=None, extra_context=None, model=None)

Generate platform-neutral title/description/keywords from a transcript.

LLM-backed via `aix`. Pass a ready [`ContentMetadata`](_autosummary/yb.content.html.md#yb.content.ContentMetadata) to
[`prepare_content()`](_autosummary/yb.content.html.md#yb.content.prepare_content) instead if you want to skip generation.

* **Raises:**
  [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – `aix` is not importable.
* **Return type:**
  [`ContentMetadata`](_autosummary/yb.content.html.md#yb.content.ContentMetadata)

### yb.content.prepare_content(media, , language='English', language_code=None, audio_language_code=None, brand=None, extra_context=None, transcript=None, with_chapters=True, with_thumbnail=False, thumbnail_text=None, metadata=None, model=None, transcribe_kwargs=None, chapters_kwargs=None)

Build a [`PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent) for `media` (destination-agnostic).

Ensures a persisted SRT next to the media (transcribing once via
`mixing` if needed), writes LLM metadata, detects chapters (default on,
auto-skipped for clips too short to host them), and optionally renders a
thumbnail. Any precomputed pieces (`transcript`, `metadata`) are reused
instead of recomputed.

* **Parameters:**
  * **media** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Audio or video file.
  * **language** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Human-readable language to write metadata in.
  * **audio_language_code** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – BCP-47 codes for the metadata
    text and the spoken audio.
  * **brand** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Product/brand name to keep verbatim in the copy.
  * **extra_context** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Extra guidance for the copywriter (audience, CTA…).
  * **transcript** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – SRT text to use as-is (skips transcription).
  * **with_chapters** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Detect chapters (default `True`).
  * **with_thumbnail** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Render a thumbnail image (default `False`).
  * **thumbnail_text** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Overlay text for the thumbnail (defaults to the title).
  * **metadata** ([`ContentMetadata`](_autosummary/yb.content.html.md#yb.content.ContentMetadata) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Precomputed [`ContentMetadata`](_autosummary/yb.content.html.md#yb.content.ContentMetadata) to reuse.
  * **model** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – LLM model override.
  * **chapters_kwargs** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Forwarded to the mixing calls.
* **Return type:**
  [`PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent)
* **Returns:**
  A populated [`PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent).


# _autosummary/yb.download.html.md

# yb.download

Media download (yt-dlp) — fetch videos, audio, and their metadata.

A separate, optionally-installed module (`pip install 'yb[download]'`).
Common use:

```pycon
>>> from yb.download import download_youtube_video
>>> result = download_youtube_video("https://youtu.be/PRa9ciOe-us")
>>> print(result.path, result.video_id)
```

For audio only — e.g. to feed a transcription or podcast workflow — use
[`download_youtube_audio()`](_autosummary/yb.download.html.md#yb.download.download_youtube_audio), which keeps the source format by default and
converts only when you ask it to (conversion needs ffmpeg):

```pycon
>>> from yb.download import download_youtube_audio
>>> ep = download_youtube_audio(url, audio_format="mp3")
```

For sound or clip sourcing, fetch just a span (seconds or `"mm:ss"`; needs
ffmpeg, see [`yb.download.ranges`](_autosummary/yb.download.ranges.html.md#module-yb.download.ranges)). The span is recorded in
`result.info["download_range"]` for provenance:

```pycon
>>> clip = download_youtube_audio(url, start="0:07.9", end="0:08.75")
```

YouTube downloads need a JavaScript runtime (Deno) and yt-dlp’s solver
scripts; [`check_requirements()`](_autosummary/yb.download.html.md#yb.download.check_requirements) (or `yb check`) says what’s missing and
how to install it. The first download in a process runs it and warns.

### Functions

| [`download_youtube_video`](_autosummary/yb.download.html.md#yb.download.download_youtube_video)(url, \*[, ...])         | Download a YouTube video to `download_dir` (default `~/Downloads`).           |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`download_youtube_audio`](_autosummary/yb.download.html.md#yb.download.download_youtube_audio)(url, \*[, ...])         | Download only a video's audio (no video stream).                              |
| [`youtube_video_info`](_autosummary/yb.download.html.md#yb.download.youtube_video_info)(url, \*[, extra_opts, ...]) | Fetch a video's metadata without downloading it.                              |
| [`download_youtube_playlist`](_autosummary/yb.download.html.md#yb.download.download_youtube_playlist)(url, \*[, ...])      | Download all (or a selected subset of) a YouTube playlist's videos.           |
| [`youtube_playlist_info`](_autosummary/yb.download.html.md#yb.download.youtube_playlist_info)(url, \*[, ...])          | Fetch a playlist's per-video metadata without downloading.                    |
| [`default_download_dir`](_autosummary/yb.download.html.md#yb.download.default_download_dir)()                         | Resolve the default download directory (`$YB_DOWNLOAD_DIR` or `~/Downloads`). |
| [`convert_audio`](_autosummary/yb.download.html.md#yb.download.convert_audio)(src, audio_format, \*[, ...])    | Convert `src` to `audio_format`, returning the resulting path.                |
| [`check_requirements`](_autosummary/yb.download.html.md#yb.download.check_requirements)(\*[, warn, which, ...])     | Check what YouTube downloads need, and say how to fix what's missing.         |
| [`find_js_runtime`](_autosummary/yb.download.html.md#yb.download.find_js_runtime)(\*[, which, search_dirs])      | Find Deno on `PATH`, else in its standard install location.                   |
| [`parse_timestamp`](_autosummary/yb.download.html.md#yb.download.parse_timestamp)(value)                         | Seconds from a number or a `"[[h:]m:]s[.fraction]"` string.                   |

### Classes

| [`DownloadResult`](_autosummary/yb.download.html.md#yb.download.DownloadResult)(path, info[, sidecars])   | Outcome of a download.                 |
|-------------------------------------------------------------------------------------------|----------------------------------------|
| [`JsRuntime`](_autosummary/yb.download.html.md#yb.download.JsRuntime)(name, path, on_path)           | A JavaScript runtime found for yt-dlp. |
| [`TimeRange`](_autosummary/yb.download.html.md#yb.download.TimeRange)([start, end])                  | A span of a video, in seconds.         |

### Exceptions

| [`AudioConversionError`](_autosummary/yb.download.html.md#yb.download.AudioConversionError)       | Raised when audio could not be converted to the requested format.   |
|-----------------------------------------------------------------------------|---------------------------------------------------------------------|
| [`DownloadRequirementWarning`](_autosummary/yb.download.html.md#yb.download.DownloadRequirementWarning) | A download prerequisite (JS runtime, solver scripts) looks missing. |

### *exception* yb.download.AudioConversionError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised when audio could not be converted to the requested format.

### *exception* yb.download.DownloadRequirementWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A download prerequisite (JS runtime, solver scripts) looks missing.

### *class* yb.download.DownloadResult(path, info, sidecars=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Outcome of a download.

#### path

The downloaded media file.

#### info

A trimmed metadata dict (see `_INFO_FIELDS`).

#### sidecars

Map of extra artifacts written (`info_json`, `thumbnail`,
`description`, `subtitles` → path(s)).

#### *property* video_id *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The YouTube video id.

### *class* yb.download.JsRuntime(name, path, on_path)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A JavaScript runtime found for yt-dlp.

#### name

yt-dlp’s key for the runtime (e.g. `"deno"`).

#### path

The executable.

#### on_path

Whether yt-dlp would find it by name on `PATH` unaided.

### *class* yb.download.TimeRange(start=0.0, end=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A span of a video, in seconds. `end=None` means “to the end”.

#### as_info()

The span as recorded on a result’s `info` (for provenance).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]

#### *property* label *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`"7.9-8.75"` or `"30-end"`.

* **Type:**
  Compact, filename-safe form

#### *classmethod* of(, start=None, end=None)

Build from user input; `None` when neither bound is given.

* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – On a bad timestamp, or `end` not after `start`.
* **Return type:**
  [`TimeRange`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.TimeRange) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.download.check_requirements(\*, warn=True, which=<function which>, search_dirs=None, find_spec=<function find_spec>, run=<function run>, import_module=<function import_module>, check_deno_version=True)

Check what YouTube downloads need, and say how to fix what’s missing.

Never raises. Each problem is a message ending with the fix.

* **Parameters:**
  * **warn** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also emit each problem as a [`DownloadRequirementWarning`](_autosummary/yb.download.html.md#yb.download.DownloadRequirementWarning).
  * **which** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – See [`find_js_runtime()`](_autosummary/yb.download.html.md#yb.download.find_js_runtime).
  * **search_dirs** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – See [`find_js_runtime()`](_autosummary/yb.download.html.md#yb.download.find_js_runtime).
  * **find_spec** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`object`](https://docs.python.org/3/builtins/functions.html#object) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Module lookup for `has_module()`.
  * **run** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`CompletedProcess`](https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – Subprocess runner for `deno_version()`.
  * **import_module** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`object`](https://docs.python.org/3/builtins/functions.html#object)]) – Importer for `min_deno_version()`.
  * **check_deno_version** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also run `deno --version` and compare it with
    yt-dlp’s minimum. The download preflight turns this off: it spawns
    a process, which belongs in an explicit check, not in a download.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  The problems found; empty when downloads should work.

### yb.download.convert_audio(src, audio_format, , output=None, bitrate=None, overwrite=True, extra_ffmpeg_args=())

Convert `src` to `audio_format`, returning the resulting path.

* **Parameters:**
  * **src** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – The audio file to convert.
  * **audio_format** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Target format as an extension (`"mp3"`, `".wav"`, …).
  * **output** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Destination path. Defaults to `src` with the new extension.
  * **bitrate** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Target bitrate for lossy formats (e.g. `"320k"`). Defaults to
    `DEFAULT_LOSSY_BITRATE`; ignored for `LOSSLESS_FORMATS`.
  * **overwrite** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Overwrite `output` if it already exists.
  * **extra_ffmpeg_args** ([`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Raw ffmpeg arguments, appended last so they win.
* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
* **Returns:**
  The converted file’s path — or `src` unchanged when it is already in
  the target format (no pointless re-encode, no generation loss).
* **Raises:**
  * [**FileNotFoundError**](https://docs.python.org/3/builtins/exceptions.html#FileNotFoundError) – If `src` does not exist.
  * [**AudioConversionError**](_autosummary/yb.download.html.md#yb.download.AudioConversionError) – If ffmpeg is missing, or the conversion fails.

### yb.download.default_download_dir()

Resolve the default download directory (`$YB_DOWNLOAD_DIR` or `~/Downloads`).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### yb.download.download_youtube_audio(url, , download_dir=None, audio_format=None, bitrate=None, keep_original=False, on_error='raise', fmt='bestaudio/best', filename_template='%(title)s (%(id)s).%(ext)s', write_info_json=False, write_thumbnail=False, write_description=False, write_subtitles=False, write_auto_subtitles=False, subtitle_langs=('en',), quiet=True, extra_opts=None, start=None, end=None, precise_cuts=False, cookies_file=None)

Download only a video’s audio (no video stream).

Simplest use: `download_youtube_audio(url)` → the best audio stream, kept
in whatever format the source offers (YouTube’s is usually Opus in a
`.webm` container), named `Title (video_id).webm` in `~/Downloads`.

Pass `audio_format` to get a specific format instead:

```pycon
>>> download_youtube_audio(url, audio_format="mp3")
```

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The video URL (or id).
  * **download_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Destination directory. Defaults to
    [`default_download_dir()`](_autosummary/yb.download.html.md#yb.download.default_download_dir).
  * **audio_format** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Target format as an extension (`"mp3"`, `".wav"`, …).
    The default `None` means **no conversion** — keep the downloaded
    bytes exactly as they came, which needs no ffmpeg and avoids
    re-encoding a lossy stream into another lossy format.
  * **bitrate** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Bitrate for lossy targets (e.g. `"320k"`). Ignored when
    `audio_format` is lossless or `None`.
  * **keep_original** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When converting, also keep the originally downloaded
    file (by default it is removed once converted).
  * **on_error** ([`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'raise'`, `'warn'`]) – What to do when conversion fails (ffmpeg missing or erroring).
    `"raise"` (default) propagates
    [`AudioConversionError`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.AudioConversionError); `"warn"` emits a
    warning and returns the unconverted download. Either way the
    downloaded audio is left on disk — a failed conversion never costs
    you the download.
  * **fmt** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp format selector (default best audio-only stream).
  * **filename_template** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp output template (default
    `"%(title)s (%(id)s).%(ext)s"`).
  * **write_info_json** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the full metadata as `*.info.json`.
  * **write_thumbnail** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the thumbnail image.
  * **write_description** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the description as `*.description`.
  * **write_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save uploaded subtitles for `subtitle_langs`.
  * **write_auto_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save auto-generated subtitles.
  * **subtitle_langs** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]) – Subtitle languages to fetch when subtitles are enabled.
  * **quiet** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Suppress yt-dlp console output.
  * **extra_opts** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Any additional raw yt-dlp options (merged last, so they win).
  * **start** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – 

    Download only a span, as in
    [`download_youtube_video()`](_autosummary/yb.download.html.md#yb.download.download_youtube_video):
    ```pycon
    >>> download_youtube_audio(url, start="0:07.9", end="0:08.75")
    ```
  * **end** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – 

    Download only a span, as in
    [`download_youtube_video()`](_autosummary/yb.download.html.md#yb.download.download_youtube_video):
    ```pycon
    >>> download_youtube_audio(url, start="0:07.9", end="0:08.75")
    ```
  * **precise_cuts** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – 

    Download only a span, as in
    [`download_youtube_video()`](_autosummary/yb.download.html.md#yb.download.download_youtube_video):
    ```pycon
    >>> download_youtube_audio(url, start="0:07.9", end="0:08.75")
    ```
* **Return type:**
  [`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult)
* **Returns:**
  A [`DownloadResult`](_autosummary/yb.download.html.md#yb.download.DownloadResult) whose `path` is the audio file — converted
  when `audio_format` was given and conversion succeeded.
* **Raises:**
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If `on_error` is not `"raise"` or `"warn"`.
  * [**AudioConversionError**](_autosummary/yb.download.html.md#yb.download.AudioConversionError) – If conversion fails and `on_error="raise"`.

### yb.download.download_youtube_playlist(url, , download_dir=None, playlist_items=None, skip_first=False, title_reject=None, download_archive=None, fmt='bestvideo+bestaudio/best', merge_to='mp4', filename_template='%(title)s (%(id)s).%(ext)s', write_info_json=True, write_thumbnail=False, write_description=False, write_subtitles=False, write_auto_subtitles=False, subtitle_langs=('en',), cookies_from_browser=None, extractor_args=None, quiet=True, extra_opts=None, cookies_file=None)

Download all (or a selected subset of) a YouTube playlist’s videos.

Simplest use: `download_youtube_playlist(url)` → every video downloaded best
quality merged to mp4 into `download_dir` (default `~/Downloads`), each with
its `*.info.json` sidecar (`write_info_json` defaults to `True` here, since
a playlist download is usually an archival operation).

Selecting a subset (yt-dlp `--playlist-items` is **1-based**):

> # skip the first (“PV”/intro) entry, keep the rest
> download_youtube_playlist(url, skip_first=True)         # -> playlist_items=”2:”
> download_youtube_playlist(url, playlist_items=”2:”)     # same, explicit
> download_youtube_playlist(url, playlist_items=”2”)      # only the 2nd video
> download_youtube_playlist(url, title_reject=”PV”)       # skip entries whose title contains “PV”
* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The playlist URL.
  * **download_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Destination directory (default [`default_download_dir()`](_autosummary/yb.download.html.md#yb.download.default_download_dir)).
  * **playlist_items** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – yt-dlp item selector (1-based; `"2:"`, `"2"`, `"1:5,8"`).
  * **skip_first** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Convenience for `playlist_items="2:"` (ignored if
    `playlist_items` is given).
  * **title_reject** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Skip entries whose (case-insensitive) title contains this
    substring — more robust than a positional skip if ordering changes.
  * **download_archive** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Path to a yt-dlp archive file recording downloaded ids,
    making re-runs idempotent/resumable.
  * **fmt** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp format selector (default best video + best audio).
  * **merge_to** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Container to merge into (default `"mp4"`; needs ffmpeg).
  * **filename_template** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp output template (default
    `"%(title)s (%(id)s).%(ext)s"`).
  * **write_info_json** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Save each video’s `*.info.json` (default `True`).
  * **write_thumbnail** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool))
  * **write_description** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool))
  * **write_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool))
  * **write_auto_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool))
* **Return type:**
  [*list*](https://docs.python.org/3/builtins/stdtypes.html#list)[[*DownloadResult*](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult)]

:param :
:type subtitle_langs: [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]
:param subtitle_langs: As in [`download_youtube_video()`](_autosummary/yb.download.html.md#yb.download.download_youtube_video).
:type cookies_from_browser: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple) | [`None`](https://docs.python.org/3/builtins/constants.html#None)
:param cookies_from_browser: Browser to read cookies from for bot-detection /

> age / region issues, e.g. `"safari"` or `("chrome", "Profile 1")`.
* **Parameters:**
  * **extractor_args** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – yt-dlp `extractor_args` (e.g.
    `{"youtube": {"player_client": ["web_safari"]}}`).
  * **quiet** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Suppress yt-dlp console output.
  * **extra_opts** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Any additional raw yt-dlp options (merged last, so they win).
  * **cookies_file** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`False`], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – As in [`download_youtube_video()`](_autosummary/yb.download.html.md#yb.download.download_youtube_video); an explicit
    `cookies_from_browser` takes precedence over the default file.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult)]
* **Returns:**
  A list of [`DownloadResult`](_autosummary/yb.download.html.md#yb.download.DownloadResult), one per successfully-downloaded entry,
  in playlist order. Entries skipped by selection/filter or that failed
  (under `ignoreerrors`) are omitted.

### yb.download.download_youtube_video(url, , download_dir=None, fmt='bestvideo+bestaudio/best', merge_to='mp4', filename_template='%(title)s (%(id)s).%(ext)s', write_info_json=False, write_thumbnail=False, write_description=False, write_subtitles=False, write_auto_subtitles=False, subtitle_langs=('en',), quiet=True, extra_opts=None, start=None, end=None, precise_cuts=False, cookies_file=None)

Download a YouTube video to `download_dir` (default `~/Downloads`).

Simplest use: `download_youtube_video(url)` → best quality merged to mp4,
named `Title (video_id).mp4` in `~/Downloads`.

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The video URL (or id).
  * **download_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Destination directory. Defaults to
    [`default_download_dir()`](_autosummary/yb.download.html.md#yb.download.default_download_dir).
  * **fmt** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp format selector (default best video + best audio).
  * **merge_to** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Container to merge video+audio into (default `"mp4"`;
    requires ffmpeg). Set `None` to keep yt-dlp’s default.
  * **filename_template** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp output template (default
    `"%(title)s (%(id)s).%(ext)s"`).
  * **write_info_json** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the full metadata as `*.info.json`.
  * **write_thumbnail** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the thumbnail image.
  * **write_description** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the description as `*.description`.
  * **write_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save uploaded subtitles for `subtitle_langs`.
  * **write_auto_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save auto-generated subtitles.
  * **subtitle_langs** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]) – Subtitle languages to fetch when subtitles are enabled.
  * **quiet** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Suppress yt-dlp console output.
  * **extra_opts** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Any additional raw yt-dlp options (merged last, so they win).
  * **start** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Download only this span: seconds (`7.9`) or a timestamp
    (`"0:07.9"`, `"1:02:03"`). Either may be given alone (`end`
    only = from the beginning; `start` only = to the end). The span
    is recorded in `info["download_range"]` as `{"start", "end"}`
    seconds (`end` is `None` for “to the end”), and, with the
    default `filename_template`, in the filename (`Title (id)
    [7.9-8.75].mp4`) so a clip never collides with the full video.
    Needs ffmpeg (see [`yb.download.ranges`](_autosummary/yb.download.ranges.html.md#module-yb.download.ranges)).
  * **end** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Download only this span: seconds (`7.9`) or a timestamp
    (`"0:07.9"`, `"1:02:03"`). Either may be given alone (`end`
    only = from the beginning; `start` only = to the end). The span
    is recorded in `info["download_range"]` as `{"start", "end"}`
    seconds (`end` is `None` for “to the end”), and, with the
    default `filename_template`, in the filename (`Title (id)
    [7.9-8.75].mp4`) so a clip never collides with the full video.
    Needs ffmpeg (see [`yb.download.ranges`](_autosummary/yb.download.ranges.html.md#module-yb.download.ranges)).
  * **precise_cuts** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – With `start`/`end`, re-encode at the cuts for
    frame-accurate bounds (slower) instead of cutting at keyframes.
  * **cookies_file** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`False`], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Netscape-format cookies file to authenticate with (for
    headless servers where `cookies_from_browser` cannot work). Default
    `None` reads `$YB_COOKIES_FILE`, else
    `~/.config/yb/youtube-cookies.txt` if it exists, else sends no
    cookies. An explicit `cookies_from_browser` or `extra_opts`
    cookie option suppresses that default; `False` opts out of it.
    The file is checked first (`ValueError` if explicit, else a
    warning, naming only line numbers). It cannot be combined with a
    browser cookie source. yt-dlp rewrites the file with refreshed
    cookies on exit, so it must be writable and not shared by parallel
    workers.
* **Return type:**
  [`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult)
* **Returns:**
  A [`DownloadResult`](_autosummary/yb.download.html.md#yb.download.DownloadResult) with the media path, trimmed `info`, and any
  `sidecars` written.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If `start`/`end` is not a timestamp, or `end` is not
      after `start`. Raised before anything is fetched or written.

### yb.download.find_js_runtime(\*, which=<function which>, search_dirs=None)

Find Deno on `PATH`, else in its standard install location.

* **Parameters:**
  * **which** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – `PATH` lookup (injectable for tests).
  * **search_dirs** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Extra directories to look in when it isn’t on `PATH`
    (default `deno_search_dirs()`).
* **Return type:**
  [`JsRuntime`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.JsRuntime) | [`None`](https://docs.python.org/3/builtins/constants.html#None)
* **Returns:**
  The runtime found, or `None`.

### yb.download.parse_timestamp(value)

Seconds from a number or a `"[[h:]m:]s[.fraction]"` string.

* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If `value` is negative, not finite, or not a timestamp.
* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### yb.download.youtube_playlist_info(url, , playlist_items=None, flat=True, extra_opts=None, cookies_file=None)

Fetch a playlist’s per-video metadata without downloading.

Returns a dict with playlist-level fields (`playlist_id`, `playlist_title`,
`webpage_url`, `uploader`, `count`) and an `entries` list. With
`flat=True` (default) extraction is fast/shallow (each entry has at least
`id`, `title`, `url`); with `flat=False` each entry is fully resolved
and trimmed to `_INFO_FIELDS` (slower, but includes duration/fps/etc.).

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The playlist URL.
  * **playlist_items** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – yt-dlp `--playlist-items` selector, 1-based, e.g.
    `"2:"` (all but the first), `"2"` (only the 2nd), `"1:5,8"`.
  * **flat** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Shallow vs full per-entry extraction.
  * **extra_opts** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Additional raw yt-dlp options (merged last).
  * **cookies_file** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`False`], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – See [`download_youtube_video()`](_autosummary/yb.download.html.md#yb.download.download_youtube_video).
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### yb.download.youtube_video_info(url, , extra_opts=None, cookies_file=None)

Fetch a video’s metadata without downloading it.

Returns the trimmed info dict (see `_INFO_FIELDS`). Useful to preview
the title/duration/chapters before deciding to download.

`cookies_file`: see [`download_youtube_video()`](_autosummary/yb.download.html.md#yb.download.download_youtube_video).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### Modules

| [`ranges`](_autosummary/yb.download.ranges.html.md#module-yb.download.ranges)             | Download just a time range of a video: parse start/end, build yt-dlp's options.   |
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [`requirements`](_autosummary/yb.download.requirements.html.md#module-yb.download.requirements) | Preflight for downloading: does yt-dlp have what YouTube now demands?             |
| [`youtube`](_autosummary/yb.download.youtube.html.md#module-yb.download.youtube)           | Download YouTube videos (and their metadata) via yt-dlp.                          |


# _autosummary/yb.download.ranges.html.md

# yb.download.ranges

Download just a time range of a video: parse start/end, build yt-dlp’s options.

Sourcing a sound effect or a clip from a long video should not mean downloading
the whole thing. `start=` / `end=` on [`yb.download.download_youtube_video()`](_autosummary/yb.download.html.md#yb.download.download_youtube_video)
and [`yb.download.download_youtube_audio()`](_autosummary/yb.download.html.md#yb.download.download_youtube_audio) take seconds (`7.9`) or
timestamps (`"0:07.9"`, `"1:02:03"`); this module turns them into
yt-dlp’s `download_ranges` option.

```pycon
>>> parse_timestamp("1:02.5")
62.5
>>> TimeRange.of(start="0:07.9", end=8.75).label
'7.9-8.75'
>>> TimeRange.of(start=30).label
'30-end'
```

yt-dlp cuts a range by handing the stream to **ffmpeg**, so the range path
needs ffmpeg on `PATH` (or `extra_opts={"ffmpeg_location": ...}`). Cuts land
on keyframes unless `precise_cuts=True` (re-encodes, slower, frame-accurate).
When the system ffmpeg is a build that lacks what yt-dlp asks of it, point
`ffmpeg_location` at another one (moviepy’s bundled ffmpeg, for example).

### Module Attributes

| [`Timestamp`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.Timestamp)             | seconds, or a `[[h:]m:]s[.fraction]` string.                                   |
|------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`DOWNLOAD_RANGES_OPT`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.DOWNLOAD_RANGES_OPT)   | yt-dlp option names this module sets.                                          |
| [`INFO_RANGE_KEY`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.INFO_RANGE_KEY)        | Key under which a ranged download's span is recorded on `DownloadResult.info`. |
| [`MAX_TIMESTAMP_SECONDS`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.MAX_TIMESTAMP_SECONDS) | beyond it a typo, not a video.                                                 |

### Functions

| [`default_range_func`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.default_range_func)()                               | yt-dlp's `download_range_func` (imported lazily: yt-dlp is optional).      |
|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`parse_timestamp`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.parse_timestamp)(value)                             | Seconds from a number or a `"[[h:]m:]s[.fraction]"` string.                |
| [`range_opts`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.range_opts)(time_range, \*[, precise_cuts, ...])    | The yt-dlp options that download only `time_range`.                        |
| [`ranged_filename_template`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.ranged_filename_template)(template, time_range, \*) | `template` with the range before the extension: `"T (id) [7.9-8.75].mp4"`. |

### Classes

| [`TimeRange`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.TimeRange)([start, end])   | A span of a video, in seconds.   |
|----------------------------------------------------------------------------|----------------------------------|

### yb.download.ranges.DOWNLOAD_RANGES_OPT *= 'download_ranges'*

yt-dlp option names this module sets.

### yb.download.ranges.INFO_RANGE_KEY *= 'download_range'*

Key under which a ranged download’s span is recorded on `DownloadResult.info`.

### yb.download.ranges.MAX_TIMESTAMP_SECONDS *= 864000*

beyond it a typo, not a video.

* **Type:**
  Longest timestamp accepted (10 days)

### *class* yb.download.ranges.TimeRange(start=0.0, end=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A span of a video, in seconds. `end=None` means “to the end”.

#### as_info()

The span as recorded on a result’s `info` (for provenance).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]

#### *property* label *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`"7.9-8.75"` or `"30-end"`.

* **Type:**
  Compact, filename-safe form

#### *classmethod* of(, start=None, end=None)

Build from user input; `None` when neither bound is given.

* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – On a bad timestamp, or `end` not after `start`.
* **Return type:**
  [`TimeRange`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.TimeRange) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.download.ranges.Timestamp *= int | float | str*

seconds, or a `[[h:]m:]s[.fraction]` string.

* **Type:**
  A time as callers give it

### yb.download.ranges.default_range_func()

yt-dlp’s `download_range_func` (imported lazily: yt-dlp is optional).

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### yb.download.ranges.parse_timestamp(value)

Seconds from a number or a `"[[h:]m:]s[.fraction]"` string.

* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If `value` is negative, not finite, or not a timestamp.
* **Return type:**
  [`float`](https://docs.python.org/3/builtins/functions.html#float)

### yb.download.ranges.range_opts(time_range, , precise_cuts=False, range_func=None)

The yt-dlp options that download only `time_range`.

* **Parameters:**
  * **time_range** ([`TimeRange`](_autosummary/yb.download.ranges.html.md#yb.download.ranges.TimeRange)) – The span to fetch.
  * **precise_cuts** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Re-encode at the cuts for frame-accurate bounds (slower).
  * **range_func** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Stand-in for yt-dlp’s `download_range_func` (tests).
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### yb.download.ranges.ranged_filename_template(template, time_range, , precise_cuts=False)

`template` with the range before the extension: `"T (id) [7.9-8.75].mp4"`.

Without it a clip would share the full video’s filename, and yt-dlp would
take one for the other (“already downloaded”). A precise cut is named
apart from a keyframe cut for the same reason. Only templates ending in
`.%(ext)s` (the default does) are changed; one written by hand is the
caller’s to keep distinct.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/yb.download.requirements.html.md

# yb.download.requirements

Preflight for downloading: does yt-dlp have what YouTube now demands?

YouTube makes yt-dlp solve a JavaScript “n challenge”. Without a solver it
fails late and cryptically (`No video formats found!`). Solving needs two
things:

- yt-dlp’s challenge-solver scripts, shipped by `yt-dlp[default]` (the
  `yt-dlp-ejs` package), which `yb[download]` depends on;
- a JavaScript runtime. **Deno** is the one yt-dlp enables by default, and the
  one that works.

Deno’s installer puts it in `~/.deno/bin` and only adds that to `PATH` for
interactive shells, so a cron job or service often can’t see it. This module
also looks there, and [`yt_dlp_runtime_opts()`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.yt_dlp_runtime_opts) hands yt-dlp the explicit
path, so downloads work without touching `PATH`.

```pycon
>>> from yb.download import check_requirements
>>> problems = check_requirements()
>>> print("\n\n".join(problems) or "ready")
```

Nothing here raises: missing pieces are reported (and, by default, warned) with
the command that fixes them.

### Module Attributes

| [`DENO`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.DENO)                      | The JavaScript runtime yt-dlp enables by default, and the one that works.                           |
|----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| [`DENO_INSTALL_CMD`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.DENO_INSTALL_CMD)          | Deno's official installer.                                                                          |
| [`DENO_INSTALL_ENV`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.DENO_INSTALL_ENV)          | Env var Deno's installer honours for its install root (binary in `<root>/bin`).                     |
| [`YT_DLP_MODULE`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.YT_DLP_MODULE)             | yt-dlp's import name.                                                                               |
| [`EJS_MODULE`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.EJS_MODULE)                | Module that carries yt-dlp's challenge-solver scripts (from `yt-dlp[default]`).                     |
| [`DENO_UPGRADE_CMD`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.DENO_UPGRADE_CMD)          | Command that brings an installed Deno up to date.                                                   |
| [`DENO_MIN_VERSION_FALLBACK`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.DENO_MIN_VERSION_FALLBACK) | Fallback for Deno's minimum version when yt-dlp can't be asked (not importable, or too old to say). |
| [`DENO_VERSION_TIMEOUT`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.DENO_VERSION_TIMEOUT)      | Seconds to wait for `deno --version`.                                                               |
| [`EJS_WIKI_URL`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.EJS_WIKI_URL)              | yt-dlp's notes on the JavaScript challenge.                                                         |
| [`JS_RUNTIMES_OPT`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.JS_RUNTIMES_OPT)           | The yt-dlp option naming JS runtimes (`{"deno": {"path": ...}}`).                                   |

### Functions

| [`check_requirements`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.check_requirements)(\*[, warn, which, ...])   | Check what YouTube downloads need, and say how to fix what's missing.   |
|-----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`deno_search_dirs`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.deno_search_dirs)()                           | Where Deno lives when it isn't on `PATH`.                               |
| [`deno_version`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.deno_version)(runtime, \*[, run])             | Ask `runtime` for its version (`deno --version`); `None` if unknown.    |
| [`find_js_runtime`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.find_js_runtime)(\*[, which, search_dirs])    | Find Deno on `PATH`, else in its standard install location.             |
| [`has_ejs_scripts`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.has_ejs_scripts)(\*[, find_spec])             | Whether yt-dlp's challenge-solver scripts (`yt-dlp-ejs`) are installed. |
| [`has_module`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.has_module)(name, \*[, find_spec])            | Whether module `name` is importable, without importing it.              |
| [`min_deno_version`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.min_deno_version)(\*[, import_module])        | The oldest Deno yt-dlp supports, as a version tuple.                    |
| [`with_runtime_opts`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.with_runtime_opts)(opts)                      | `opts` plus the preflight's runtime path, unless `opts` names runtimes. |
| [`yt_dlp_runtime_opts`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.yt_dlp_runtime_opts)(runtime)                 | yt-dlp options pointing at `runtime` when yt-dlp can't find it alone.   |

### Classes

| [`JsRuntime`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.JsRuntime)(name, path, on_path)   | A JavaScript runtime found for yt-dlp.   |
|-----------------------------------------------------------------------------------|------------------------------------------|

### Exceptions

| [`DownloadRequirementWarning`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.DownloadRequirementWarning)   | A download prerequisite (JS runtime, solver scripts) looks missing.   |
|-------------------------------------------------------------------------------|-----------------------------------------------------------------------|

### yb.download.requirements.DENO *= 'deno'*

The JavaScript runtime yt-dlp enables by default, and the one that works.

### yb.download.requirements.DENO_INSTALL_CMD *= 'curl -fsSL https://deno.land/install.sh | sh'*

Deno’s official installer.

### yb.download.requirements.DENO_INSTALL_ENV *= 'DENO_INSTALL'*

Env var Deno’s installer honours for its install root (binary in `<root>/bin`).

### yb.download.requirements.DENO_MIN_VERSION_FALLBACK *= (2, 3, 0)*

Fallback for Deno’s minimum version when yt-dlp can’t be asked (not
importable, or too old to say). yt-dlp’s own value wins: see
[`min_deno_version()`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.min_deno_version).

### yb.download.requirements.DENO_UPGRADE_CMD *= 'deno upgrade'*

Command that brings an installed Deno up to date.

### yb.download.requirements.DENO_VERSION_TIMEOUT *= 10*

Seconds to wait for `deno --version`.

### *exception* yb.download.requirements.DownloadRequirementWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A download prerequisite (JS runtime, solver scripts) looks missing.

### yb.download.requirements.EJS_MODULE *= 'yt_dlp_ejs'*

Module that carries yt-dlp’s challenge-solver scripts (from `yt-dlp[default]`).

### yb.download.requirements.EJS_WIKI_URL *= 'https://github.com/yt-dlp/yt-dlp/wiki/EJS'*

yt-dlp’s notes on the JavaScript challenge.

### yb.download.requirements.JS_RUNTIMES_OPT *= 'js_runtimes'*

The yt-dlp option naming JS runtimes (`{"deno": {"path": ...}}`).

### *class* yb.download.requirements.JsRuntime(name, path, on_path)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A JavaScript runtime found for yt-dlp.

#### name

yt-dlp’s key for the runtime (e.g. `"deno"`).

#### path

The executable.

#### on_path

Whether yt-dlp would find it by name on `PATH` unaided.

### yb.download.requirements.YT_DLP_MODULE *= 'yt_dlp'*

yt-dlp’s import name.

### yb.download.requirements.check_requirements(\*, warn=True, which=<function which>, search_dirs=None, find_spec=<function find_spec>, run=<function run>, import_module=<function import_module>, check_deno_version=True)

Check what YouTube downloads need, and say how to fix what’s missing.

Never raises. Each problem is a message ending with the fix.

* **Parameters:**
  * **warn** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also emit each problem as a [`DownloadRequirementWarning`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.DownloadRequirementWarning).
  * **which** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – See [`find_js_runtime()`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.find_js_runtime).
  * **search_dirs** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – See [`find_js_runtime()`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.find_js_runtime).
  * **find_spec** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`object`](https://docs.python.org/3/builtins/functions.html#object) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Module lookup for [`has_module()`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.has_module).
  * **run** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`CompletedProcess`](https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – Subprocess runner for [`deno_version()`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.deno_version).
  * **import_module** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`object`](https://docs.python.org/3/builtins/functions.html#object)]) – Importer for [`min_deno_version()`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.min_deno_version).
  * **check_deno_version** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also run `deno --version` and compare it with
    yt-dlp’s minimum. The download preflight turns this off: it spawns
    a process, which belongs in an explicit check, not in a download.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  The problems found; empty when downloads should work.

### yb.download.requirements.deno_search_dirs()

Where Deno lives when it isn’t on `PATH`.

This Python’s scripts dir and its interpreter’s dir (a pip-installed Deno,
e.g. `yt-dlp[deno]`, in a venv that isn’t activated: yt-dlp looks there
too), then the installer’s `$DENO_INSTALL/bin` and `~/.deno/bin`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### yb.download.requirements.deno_version(runtime, \*, run=<function run>)

Ask `runtime` for its version (`deno --version`); `None` if unknown.

Never raises: a runtime that can’t be run, or prints something unexpected,
has an unknown version (which [`check_requirements()`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.check_requirements) doesn’t flag).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.download.requirements.find_js_runtime(\*, which=<function which>, search_dirs=None)

Find Deno on `PATH`, else in its standard install location.

* **Parameters:**
  * **which** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – `PATH` lookup (injectable for tests).
  * **search_dirs** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Extra directories to look in when it isn’t on `PATH`
    (default [`deno_search_dirs()`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.deno_search_dirs)).
* **Return type:**
  [`JsRuntime`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.JsRuntime) | [`None`](https://docs.python.org/3/builtins/constants.html#None)
* **Returns:**
  The runtime found, or `None`.

### yb.download.requirements.has_ejs_scripts(\*, find_spec=<function find_spec>)

Whether yt-dlp’s challenge-solver scripts (`yt-dlp-ejs`) are installed.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### yb.download.requirements.has_module(name, \*, find_spec=<function find_spec>)

Whether module `name` is importable, without importing it.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### yb.download.requirements.min_deno_version(\*, import_module=<function import_module>)

The oldest Deno yt-dlp supports, as a version tuple.

Read from yt-dlp (`DenoJsRuntime.MIN_SUPPORTED_VERSION`) so it follows
yt-dlp’s upgrades; [`DENO_MIN_VERSION_FALLBACK`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.DENO_MIN_VERSION_FALLBACK) when yt-dlp isn’t
importable or doesn’t say.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`int`](https://docs.python.org/3/builtins/functions.html#int), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### yb.download.requirements.with_runtime_opts(opts)

`opts` plus the preflight’s runtime path, unless `opts` names runtimes.

The first call per process runs [`check_requirements()`](_autosummary/yb.download.requirements.html.md#yb.download.requirements.check_requirements) (warning, never
raising). An explicit `js_runtimes` in `opts` always wins, and skips the
check: the caller has chosen a runtime.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.download.requirements.yt_dlp_runtime_opts(runtime)

yt-dlp options pointing at `runtime` when yt-dlp can’t find it alone.

Empty when there’s nothing to add (no runtime, or it’s already on `PATH`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]]


# _autosummary/yb.download.youtube.html.md

# yb.download.youtube

Download YouTube videos (and their metadata) via yt-dlp.

The common case is one call — `download_youtube_video(url)` — which fetches
the best video+audio into `~/Downloads` as `Title (video_id).mp4`. Every
knob (destination, format, filename template, and which sidecar metadata to
also save) is overridable, and any raw yt-dlp option can be passed through.

The destination defaults to `$YB_DOWNLOAD_DIR` when set, else `~/Downloads`.

### Module Attributes

| [`OnConvertError`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.OnConvertError)       | What [`download_youtube_audio()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_audio) does when a format conversion fails.     |
|-----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| [`DOWNLOAD_DIR_ENV`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DOWNLOAD_DIR_ENV)     | Env var that overrides the default download directory.                                                                  |
| [`COOKIES_FILE_ENV`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.COOKIES_FILE_ENV)     | Env var naming a Netscape-format cookies file to authenticate downloads with.                                           |
| [`DEFAULT_COOKIES_FILE`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DEFAULT_COOKIES_FILE) | Cookies file used when [`COOKIES_FILE_ENV`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.COOKIES_FILE_ENV) is unset and this file exists. |
| [`CookiesFile`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.CookiesFile)          | a path, `None` (look for a default), or `False` (none).                                                                 |
| [`DEFAULT_OUTTMPL`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DEFAULT_OUTTMPL)      | human title plus the stable id, e.g. "My Talk (dQw4...).mp4".                                                           |
| [`DEFAULT_AUDIO_FMT`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DEFAULT_AUDIO_FMT)    | yt-dlp format selector for the best audio-only stream.                                                                  |

### Functions

| [`default_cookies_file`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.default_cookies_file)()                         | The cookies file downloads use when none is passed, or `None`.                |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`default_download_dir`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.default_download_dir)()                         | Resolve the default download directory (`$YB_DOWNLOAD_DIR` or `~/Downloads`). |
| [`download_youtube_audio`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_audio)(url, \*[, ...])         | Download only a video's audio (no video stream).                              |
| [`download_youtube_playlist`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_playlist)(url, \*[, ...])      | Download all (or a selected subset of) a YouTube playlist's videos.           |
| [`download_youtube_video`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_video)(url, \*[, ...])         | Download a YouTube video to `download_dir` (default `~/Downloads`).           |
| [`glob_escape`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.glob_escape)(name)                              | Escape glob metacharacters in a literal filename stem.                        |
| [`youtube_playlist_info`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.youtube_playlist_info)(url, \*[, ...])          | Fetch a playlist's per-video metadata without downloading.                    |
| [`youtube_video_info`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.youtube_video_info)(url, \*[, extra_opts, ...]) | Fetch a video's metadata without downloading it.                              |

### Classes

| [`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult)(path, info[, sidecars])   | Outcome of a download.   |
|-------------------------------------------------------------------------------------------|--------------------------|

### yb.download.youtube.COOKIES_FILE_ENV *= 'YB_COOKIES_FILE'*

Env var naming a Netscape-format cookies file to authenticate downloads with.

### yb.download.youtube.CookiesFile

a path, `None` (look for a default), or `False` (none).

* **Type:**
  What `cookies_file` accepts

alias of [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[False] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.download.youtube.DEFAULT_AUDIO_FMT *= 'bestaudio/best'*

yt-dlp format selector for the best audio-only stream.

### yb.download.youtube.DEFAULT_COOKIES_FILE *= '~/.config/yb/youtube-cookies.txt'*

Cookies file used when [`COOKIES_FILE_ENV`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.COOKIES_FILE_ENV) is unset and this file exists.

### yb.download.youtube.DEFAULT_OUTTMPL *= '%(title)s (%(id)s).%(ext)s'*

human title plus the stable id, e.g. “My Talk (dQw4…).mp4”.

* **Type:**
  yt-dlp output template

### yb.download.youtube.DOWNLOAD_DIR_ENV *= 'YB_DOWNLOAD_DIR'*

Env var that overrides the default download directory.

### *class* yb.download.youtube.DownloadResult(path, info, sidecars=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Outcome of a download.

#### path

The downloaded media file.

#### info

A trimmed metadata dict (see `_INFO_FIELDS`).

#### sidecars

Map of extra artifacts written (`info_json`, `thumbnail`,
`description`, `subtitles` → path(s)).

#### *property* video_id *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The YouTube video id.

### yb.download.youtube.OnConvertError

What [`download_youtube_audio()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_audio) does when a format conversion fails.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘raise’, ‘warn’]

### yb.download.youtube.default_cookies_file()

The cookies file downloads use when none is passed, or `None`.

`$YB_COOKIES_FILE` if set, else `~/.config/yb/youtube-cookies.txt` if it
exists. A file named by the env var that does not exist is skipped with a
warning (a typo should not silently fall back to anonymous downloads).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.download.youtube.default_download_dir()

Resolve the default download directory (`$YB_DOWNLOAD_DIR` or `~/Downloads`).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### yb.download.youtube.download_youtube_audio(url, , download_dir=None, audio_format=None, bitrate=None, keep_original=False, on_error='raise', fmt='bestaudio/best', filename_template='%(title)s (%(id)s).%(ext)s', write_info_json=False, write_thumbnail=False, write_description=False, write_subtitles=False, write_auto_subtitles=False, subtitle_langs=('en',), quiet=True, extra_opts=None, start=None, end=None, precise_cuts=False, cookies_file=None)

Download only a video’s audio (no video stream).

Simplest use: `download_youtube_audio(url)` → the best audio stream, kept
in whatever format the source offers (YouTube’s is usually Opus in a
`.webm` container), named `Title (video_id).webm` in `~/Downloads`.

Pass `audio_format` to get a specific format instead:

```pycon
>>> download_youtube_audio(url, audio_format="mp3")
```

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The video URL (or id).
  * **download_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Destination directory. Defaults to
    [`default_download_dir()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.default_download_dir).
  * **audio_format** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Target format as an extension (`"mp3"`, `".wav"`, …).
    The default `None` means **no conversion** — keep the downloaded
    bytes exactly as they came, which needs no ffmpeg and avoids
    re-encoding a lossy stream into another lossy format.
  * **bitrate** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Bitrate for lossy targets (e.g. `"320k"`). Ignored when
    `audio_format` is lossless or `None`.
  * **keep_original** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When converting, also keep the originally downloaded
    file (by default it is removed once converted).
  * **on_error** ([`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'raise'`, `'warn'`]) – What to do when conversion fails (ffmpeg missing or erroring).
    `"raise"` (default) propagates
    [`AudioConversionError`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.AudioConversionError); `"warn"` emits a
    warning and returns the unconverted download. Either way the
    downloaded audio is left on disk — a failed conversion never costs
    you the download.
  * **fmt** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp format selector (default best audio-only stream).
  * **filename_template** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp output template (default
    `"%(title)s (%(id)s).%(ext)s"`).
  * **write_info_json** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the full metadata as `*.info.json`.
  * **write_thumbnail** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the thumbnail image.
  * **write_description** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the description as `*.description`.
  * **write_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save uploaded subtitles for `subtitle_langs`.
  * **write_auto_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save auto-generated subtitles.
  * **subtitle_langs** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]) – Subtitle languages to fetch when subtitles are enabled.
  * **quiet** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Suppress yt-dlp console output.
  * **extra_opts** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Any additional raw yt-dlp options (merged last, so they win).
  * **start** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – 

    Download only a span, as in
    [`download_youtube_video()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_video):
    ```pycon
    >>> download_youtube_audio(url, start="0:07.9", end="0:08.75")
    ```
  * **end** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – 

    Download only a span, as in
    [`download_youtube_video()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_video):
    ```pycon
    >>> download_youtube_audio(url, start="0:07.9", end="0:08.75")
    ```
  * **precise_cuts** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – 

    Download only a span, as in
    [`download_youtube_video()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_video):
    ```pycon
    >>> download_youtube_audio(url, start="0:07.9", end="0:08.75")
    ```
* **Return type:**
  [`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult)
* **Returns:**
  A [`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult) whose `path` is the audio file — converted
  when `audio_format` was given and conversion succeeded.
* **Raises:**
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If `on_error` is not `"raise"` or `"warn"`.
  * [**AudioConversionError**](_autosummary/yb.audio_convert.html.md#yb.audio_convert.AudioConversionError) – If conversion fails and `on_error="raise"`.

### yb.download.youtube.download_youtube_playlist(url, , download_dir=None, playlist_items=None, skip_first=False, title_reject=None, download_archive=None, fmt='bestvideo+bestaudio/best', merge_to='mp4', filename_template='%(title)s (%(id)s).%(ext)s', write_info_json=True, write_thumbnail=False, write_description=False, write_subtitles=False, write_auto_subtitles=False, subtitle_langs=('en',), cookies_from_browser=None, extractor_args=None, quiet=True, extra_opts=None, cookies_file=None)

Download all (or a selected subset of) a YouTube playlist’s videos.

Simplest use: `download_youtube_playlist(url)` → every video downloaded best
quality merged to mp4 into `download_dir` (default `~/Downloads`), each with
its `*.info.json` sidecar (`write_info_json` defaults to `True` here, since
a playlist download is usually an archival operation).

Selecting a subset (yt-dlp `--playlist-items` is **1-based**):

> # skip the first (“PV”/intro) entry, keep the rest
> download_youtube_playlist(url, skip_first=True)         # -> playlist_items=”2:”
> download_youtube_playlist(url, playlist_items=”2:”)     # same, explicit
> download_youtube_playlist(url, playlist_items=”2”)      # only the 2nd video
> download_youtube_playlist(url, title_reject=”PV”)       # skip entries whose title contains “PV”
* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The playlist URL.
  * **download_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Destination directory (default [`default_download_dir()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.default_download_dir)).
  * **playlist_items** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – yt-dlp item selector (1-based; `"2:"`, `"2"`, `"1:5,8"`).
  * **skip_first** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Convenience for `playlist_items="2:"` (ignored if
    `playlist_items` is given).
  * **title_reject** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Skip entries whose (case-insensitive) title contains this
    substring — more robust than a positional skip if ordering changes.
  * **download_archive** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Path to a yt-dlp archive file recording downloaded ids,
    making re-runs idempotent/resumable.
  * **fmt** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp format selector (default best video + best audio).
  * **merge_to** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Container to merge into (default `"mp4"`; needs ffmpeg).
  * **filename_template** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp output template (default
    `"%(title)s (%(id)s).%(ext)s"`).
  * **write_info_json** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Save each video’s `*.info.json` (default `True`).
  * **write_thumbnail** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool))
  * **write_description** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool))
  * **write_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool))
  * **write_auto_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool))
* **Return type:**
  [*list*](https://docs.python.org/3/builtins/stdtypes.html#list)[[*DownloadResult*](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult)]

:param :
:type subtitle_langs: [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]
:param subtitle_langs: As in [`download_youtube_video()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_video).
:type cookies_from_browser: [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple) | [`None`](https://docs.python.org/3/builtins/constants.html#None)
:param cookies_from_browser: Browser to read cookies from for bot-detection /

> age / region issues, e.g. `"safari"` or `("chrome", "Profile 1")`.
* **Parameters:**
  * **extractor_args** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – yt-dlp `extractor_args` (e.g.
    `{"youtube": {"player_client": ["web_safari"]}}`).
  * **quiet** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Suppress yt-dlp console output.
  * **extra_opts** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Any additional raw yt-dlp options (merged last, so they win).
  * **cookies_file** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`False`], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – As in [`download_youtube_video()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_video); an explicit
    `cookies_from_browser` takes precedence over the default file.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult)]
* **Returns:**
  A list of [`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult), one per successfully-downloaded entry,
  in playlist order. Entries skipped by selection/filter or that failed
  (under `ignoreerrors`) are omitted.

### yb.download.youtube.download_youtube_video(url, , download_dir=None, fmt='bestvideo+bestaudio/best', merge_to='mp4', filename_template='%(title)s (%(id)s).%(ext)s', write_info_json=False, write_thumbnail=False, write_description=False, write_subtitles=False, write_auto_subtitles=False, subtitle_langs=('en',), quiet=True, extra_opts=None, start=None, end=None, precise_cuts=False, cookies_file=None)

Download a YouTube video to `download_dir` (default `~/Downloads`).

Simplest use: `download_youtube_video(url)` → best quality merged to mp4,
named `Title (video_id).mp4` in `~/Downloads`.

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The video URL (or id).
  * **download_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Destination directory. Defaults to
    [`default_download_dir()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.default_download_dir).
  * **fmt** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp format selector (default best video + best audio).
  * **merge_to** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Container to merge video+audio into (default `"mp4"`;
    requires ffmpeg). Set `None` to keep yt-dlp’s default.
  * **filename_template** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp output template (default
    `"%(title)s (%(id)s).%(ext)s"`).
  * **write_info_json** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the full metadata as `*.info.json`.
  * **write_thumbnail** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the thumbnail image.
  * **write_description** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the description as `*.description`.
  * **write_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save uploaded subtitles for `subtitle_langs`.
  * **write_auto_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save auto-generated subtitles.
  * **subtitle_langs** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]) – Subtitle languages to fetch when subtitles are enabled.
  * **quiet** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Suppress yt-dlp console output.
  * **extra_opts** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Any additional raw yt-dlp options (merged last, so they win).
  * **start** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Download only this span: seconds (`7.9`) or a timestamp
    (`"0:07.9"`, `"1:02:03"`). Either may be given alone (`end`
    only = from the beginning; `start` only = to the end). The span
    is recorded in `info["download_range"]` as `{"start", "end"}`
    seconds (`end` is `None` for “to the end”), and, with the
    default `filename_template`, in the filename (`Title (id)
    [7.9-8.75].mp4`) so a clip never collides with the full video.
    Needs ffmpeg (see [`yb.download.ranges`](_autosummary/yb.download.ranges.html.md#module-yb.download.ranges)).
  * **end** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Download only this span: seconds (`7.9`) or a timestamp
    (`"0:07.9"`, `"1:02:03"`). Either may be given alone (`end`
    only = from the beginning; `start` only = to the end). The span
    is recorded in `info["download_range"]` as `{"start", "end"}`
    seconds (`end` is `None` for “to the end”), and, with the
    default `filename_template`, in the filename (`Title (id)
    [7.9-8.75].mp4`) so a clip never collides with the full video.
    Needs ffmpeg (see [`yb.download.ranges`](_autosummary/yb.download.ranges.html.md#module-yb.download.ranges)).
  * **precise_cuts** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – With `start`/`end`, re-encode at the cuts for
    frame-accurate bounds (slower) instead of cutting at keyframes.
  * **cookies_file** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`False`], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Netscape-format cookies file to authenticate with (for
    headless servers where `cookies_from_browser` cannot work). Default
    `None` reads `$YB_COOKIES_FILE`, else
    `~/.config/yb/youtube-cookies.txt` if it exists, else sends no
    cookies. An explicit `cookies_from_browser` or `extra_opts`
    cookie option suppresses that default; `False` opts out of it.
    The file is checked first (`ValueError` if explicit, else a
    warning, naming only line numbers). It cannot be combined with a
    browser cookie source. yt-dlp rewrites the file with refreshed
    cookies on exit, so it must be writable and not shared by parallel
    workers.
* **Return type:**
  [`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult)
* **Returns:**
  A [`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult) with the media path, trimmed `info`, and any
  `sidecars` written.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If `start`/`end` is not a timestamp, or `end` is not
      after `start`. Raised before anything is fetched or written.

### yb.download.youtube.glob_escape(name)

Escape glob metacharacters in a literal filename stem.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.download.youtube.youtube_playlist_info(url, , playlist_items=None, flat=True, extra_opts=None, cookies_file=None)

Fetch a playlist’s per-video metadata without downloading.

Returns a dict with playlist-level fields (`playlist_id`, `playlist_title`,
`webpage_url`, `uploader`, `count`) and an `entries` list. With
`flat=True` (default) extraction is fast/shallow (each entry has at least
`id`, `title`, `url`); with `flat=False` each entry is fully resolved
and trimmed to `_INFO_FIELDS` (slower, but includes duration/fps/etc.).

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The playlist URL.
  * **playlist_items** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – yt-dlp `--playlist-items` selector, 1-based, e.g.
    `"2:"` (all but the first), `"2"` (only the 2nd), `"1:5,8"`.
  * **flat** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Shallow vs full per-entry extraction.
  * **extra_opts** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Additional raw yt-dlp options (merged last).
  * **cookies_file** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`False`], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – See [`download_youtube_video()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_video).
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### yb.download.youtube.youtube_video_info(url, , extra_opts=None, cookies_file=None)

Fetch a video’s metadata without downloading it.

Returns the trimmed info dict (see `_INFO_FIELDS`). Useful to preview
the title/duration/chapters before deciding to download.

`cookies_file`: see [`download_youtube_video()`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.download_youtube_video).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]


# _autosummary/yb.html.md

# yb

yb — streamline media publishing: prepare once, publish to YouTube or podcast.

Layered by separation of concerns:

- **core (always available):** [`yb.content`](_autosummary/yb.content.html.md#module-yb.content) — build a platform-neutral
  [`PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent) (title, description, keywords,
  > chapters, captions, thumbnail) from a media file, delegating transcription,
  > chapter detection, and thumbnails to the `mixing` package.
- **adapters (optional extras):**
  - [`yb.youtube`](_autosummary/yb.youtube.html.md#module-yb.youtube) (`pip install 'yb[youtube]'`) — upload and edit videos.
  - `yb.music` (`pip install 'yb[music]'`) — turn songs into music videos
  > (rendering via `muvid`) and publish them, including a whole folder as an
  > album. Uploading also needs `yb[youtube]`.
  - [`yb.podcast`](_autosummary/yb.podcast.html.md#module-yb.podcast) (`pip install 'yb[podcast]'`) — show notes, chapter
    markers, cover-over-audio video (via `muvid`), RSS episode item.
  - [`yb.download`](_autosummary/yb.download.html.md#module-yb.download) (`pip install 'yb[download]'`) — fetch videos/metadata.

The audio→video rendering itself lives in `muvid.visualize` (`yb` is the
publication layer); `yb.music` is the thin publish-facing facade over it.

The most common callables are re-exported here for convenience; the ones that
live in optional extras are imported lazily, so `import yb` never fails for a
missing extra — the error surfaces only when you actually use that feature.

### Examples

```pycon
>>> import yb
>>> r = yb.download_youtube_video("https://youtu.be/PRa9ciOe-us")  # needs yb[download]
>>> content = yb.prepare_content(r.path, language="English", language_code="en")
>>> yb.prepare_and_publish(r.path, privacy_status="unlisted")        # needs yb[youtube]
```

### *class* yb.ContentMetadata(title, description, keywords=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Platform-neutral copy: a title, a description, and keywords.

### *class* yb.PublicationContent(media, title='', description='', keywords=<factory>, chapters=<factory>, language=None, audio_language=None, srt_path=None, thumbnail=None, duration=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Everything needed to publish a piece of media, destination-agnostic.

#### media

Path to the audio/video file.

#### title

Headline/title.

#### description

Long-form description / show notes body.

#### keywords

Topical keywords/tags.

#### chapters

Ordered chapter markers (`mixing.chapters.Chapter`).

#### language

BCP-47 code of the metadata text (e.g. `"en"`).

#### audio_language

BCP-47 code of the spoken audio (e.g. `"fr"`).

#### srt_path

Path to the SRT subtitle/caption file, if any.

#### thumbnail

Path to a thumbnail/cover image, if any.

#### duration

Media duration in seconds, if known.

#### description_with_chapters()

Description with a chapters block appended (if any chapters).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### *class* yb.YbConfig(privacy_status='unlisted', playlist=None, create_playlist_if_missing=True, playlist_privacy_status='private')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Resolved publishing defaults.

#### privacy_status

Privacy a new upload gets (`unlisted` | `private` |
`public`) when the call doesn’t override it.

#### playlist

Title of a playlist every upload is added to (`None` = none).

#### create_playlist_if_missing

Create `playlist` if no playlist of that
title exists yet, rather than erroring.

#### playlist_privacy_status

Privacy for an auto-created playlist.

#### *classmethod* from_mapping(mapping)

Build a config from a mapping, ignoring unknown keys.

* **Return type:**
  [`YbConfig`](_autosummary/yb.config.html.md#yb.config.YbConfig)

### yb.add_video_to_playlist(video_id, title, , create=True, privacy_status='private', skip_if_present=True, service=None, \*\*cred_kwargs)

Find-or-create the playlist named `title` and append `video_id`.

Idempotent: with `skip_if_present` (default), a video already in the
playlist is left alone instead of duplicated.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  `{"playlist_id", "playlist_title", "added", "created"}` where `added`
  is `False` if the video was already present and `created` is `True`
  if the playlist had to be made.
* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – the playlist is absent and `create=False`.

### yb.default_config_file()

Config file location (`$XDG_CONFIG_HOME` or `~/.config`).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### yb.download_youtube_audio(url, , download_dir=None, audio_format=None, bitrate=None, keep_original=False, on_error='raise', fmt='bestaudio/best', filename_template='%(title)s (%(id)s).%(ext)s', write_info_json=False, write_thumbnail=False, write_description=False, write_subtitles=False, write_auto_subtitles=False, subtitle_langs=('en',), quiet=True, extra_opts=None, start=None, end=None, precise_cuts=False, cookies_file=None)

Download only a video’s audio (no video stream).

Simplest use: `download_youtube_audio(url)` → the best audio stream, kept
in whatever format the source offers (YouTube’s is usually Opus in a
`.webm` container), named `Title (video_id).webm` in `~/Downloads`.

Pass `audio_format` to get a specific format instead:

```pycon
>>> download_youtube_audio(url, audio_format="mp3")
```

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The video URL (or id).
  * **download_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Destination directory. Defaults to
    `default_download_dir()`.
  * **audio_format** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Target format as an extension (`"mp3"`, `".wav"`, …).
    The default `None` means **no conversion** — keep the downloaded
    bytes exactly as they came, which needs no ffmpeg and avoids
    re-encoding a lossy stream into another lossy format.
  * **bitrate** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Bitrate for lossy targets (e.g. `"320k"`). Ignored when
    `audio_format` is lossless or `None`.
  * **keep_original** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When converting, also keep the originally downloaded
    file (by default it is removed once converted).
  * **on_error** ([`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'raise'`, `'warn'`]) – What to do when conversion fails (ffmpeg missing or erroring).
    `"raise"` (default) propagates
    [`AudioConversionError`](_autosummary/yb.audio_convert.html.md#yb.audio_convert.AudioConversionError); `"warn"` emits a
    warning and returns the unconverted download. Either way the
    downloaded audio is left on disk — a failed conversion never costs
    you the download.
  * **fmt** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp format selector (default best audio-only stream).
  * **filename_template** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp output template (default
    `"%(title)s (%(id)s).%(ext)s"`).
  * **write_info_json** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the full metadata as `*.info.json`.
  * **write_thumbnail** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the thumbnail image.
  * **write_description** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the description as `*.description`.
  * **write_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save uploaded subtitles for `subtitle_langs`.
  * **write_auto_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save auto-generated subtitles.
  * **subtitle_langs** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]) – Subtitle languages to fetch when subtitles are enabled.
  * **quiet** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Suppress yt-dlp console output.
  * **extra_opts** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Any additional raw yt-dlp options (merged last, so they win).
  * **start** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – 

    Download only a span, as in
    [`download_youtube_video()`](_autosummary/yb.html.md#yb.download_youtube_video):
    ```pycon
    >>> download_youtube_audio(url, start="0:07.9", end="0:08.75")
    ```
  * **end** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – 

    Download only a span, as in
    [`download_youtube_video()`](_autosummary/yb.html.md#yb.download_youtube_video):
    ```pycon
    >>> download_youtube_audio(url, start="0:07.9", end="0:08.75")
    ```
  * **precise_cuts** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – 

    Download only a span, as in
    [`download_youtube_video()`](_autosummary/yb.html.md#yb.download_youtube_video):
    ```pycon
    >>> download_youtube_audio(url, start="0:07.9", end="0:08.75")
    ```
* **Return type:**
  [`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult)
* **Returns:**
  A `DownloadResult` whose `path` is the audio file — converted
  when `audio_format` was given and conversion succeeded.
* **Raises:**
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If `on_error` is not `"raise"` or `"warn"`.
  * [**AudioConversionError**](_autosummary/yb.audio_convert.html.md#yb.audio_convert.AudioConversionError) – If conversion fails and `on_error="raise"`.

### yb.download_youtube_video(url, , download_dir=None, fmt='bestvideo+bestaudio/best', merge_to='mp4', filename_template='%(title)s (%(id)s).%(ext)s', write_info_json=False, write_thumbnail=False, write_description=False, write_subtitles=False, write_auto_subtitles=False, subtitle_langs=('en',), quiet=True, extra_opts=None, start=None, end=None, precise_cuts=False, cookies_file=None)

Download a YouTube video to `download_dir` (default `~/Downloads`).

Simplest use: `download_youtube_video(url)` → best quality merged to mp4,
named `Title (video_id).mp4` in `~/Downloads`.

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The video URL (or id).
  * **download_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Destination directory. Defaults to
    `default_download_dir()`.
  * **fmt** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp format selector (default best video + best audio).
  * **merge_to** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Container to merge video+audio into (default `"mp4"`;
    requires ffmpeg). Set `None` to keep yt-dlp’s default.
  * **filename_template** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – yt-dlp output template (default
    `"%(title)s (%(id)s).%(ext)s"`).
  * **write_info_json** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the full metadata as `*.info.json`.
  * **write_thumbnail** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the thumbnail image.
  * **write_description** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save the description as `*.description`.
  * **write_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save uploaded subtitles for `subtitle_langs`.
  * **write_auto_subtitles** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also save auto-generated subtitles.
  * **subtitle_langs** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]) – Subtitle languages to fetch when subtitles are enabled.
  * **quiet** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Suppress yt-dlp console output.
  * **extra_opts** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Any additional raw yt-dlp options (merged last, so they win).
  * **start** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Download only this span: seconds (`7.9`) or a timestamp
    (`"0:07.9"`, `"1:02:03"`). Either may be given alone (`end`
    only = from the beginning; `start` only = to the end). The span
    is recorded in `info["download_range"]` as `{"start", "end"}`
    seconds (`end` is `None` for “to the end”), and, with the
    default `filename_template`, in the filename (`Title (id)
    [7.9-8.75].mp4`) so a clip never collides with the full video.
    Needs ffmpeg (see [`yb.download.ranges`](_autosummary/yb.download.ranges.html.md#module-yb.download.ranges)).
  * **end** ([`int`](https://docs.python.org/3/builtins/functions.html#int) | [`float`](https://docs.python.org/3/builtins/functions.html#float) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Download only this span: seconds (`7.9`) or a timestamp
    (`"0:07.9"`, `"1:02:03"`). Either may be given alone (`end`
    only = from the beginning; `start` only = to the end). The span
    is recorded in `info["download_range"]` as `{"start", "end"}`
    seconds (`end` is `None` for “to the end”), and, with the
    default `filename_template`, in the filename (`Title (id)
    [7.9-8.75].mp4`) so a clip never collides with the full video.
    Needs ffmpeg (see [`yb.download.ranges`](_autosummary/yb.download.ranges.html.md#module-yb.download.ranges)).
  * **precise_cuts** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – With `start`/`end`, re-encode at the cuts for
    frame-accurate bounds (slower) instead of cutting at keyframes.
  * **cookies_file** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path), [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`False`], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Netscape-format cookies file to authenticate with (for
    headless servers where `cookies_from_browser` cannot work). Default
    `None` reads `$YB_COOKIES_FILE`, else
    `~/.config/yb/youtube-cookies.txt` if it exists, else sends no
    cookies. An explicit `cookies_from_browser` or `extra_opts`
    cookie option suppresses that default; `False` opts out of it.
    The file is checked first (`ValueError` if explicit, else a
    warning, naming only line numbers). It cannot be combined with a
    browser cookie source. yt-dlp rewrites the file with refreshed
    cookies on exit, so it must be writable and not shared by parallel
    workers.
* **Return type:**
  [`DownloadResult`](_autosummary/yb.download.youtube.html.md#yb.download.youtube.DownloadResult)
* **Returns:**
  A `DownloadResult` with the media path, trimmed `info`, and any
  `sidecars` written.
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If `start`/`end` is not a timestamp, or `end` is not
      after `start`. Raised before anything is fetched or written.

### yb.format_chapter_lines(chapters)

Render chapters as `M:SS Title` lines (the shared text convention).

Uses `H:MM:SS` when any chapter is at or beyond one hour. This is the
text format both YouTube descriptions and podcast show notes accept.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.generate_metadata(transcript, , language='English', brand=None, extra_context=None, model=None)

Generate platform-neutral title/description/keywords from a transcript.

LLM-backed via `aix`. Pass a ready [`ContentMetadata`](_autosummary/yb.html.md#yb.ContentMetadata) to
[`prepare_content()`](_autosummary/yb.html.md#yb.prepare_content) instead if you want to skip generation.

* **Raises:**
  [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – `aix` is not importable.
* **Return type:**
  [`ContentMetadata`](_autosummary/yb.content.html.md#yb.content.ContentMetadata)

### yb.load_config(config_file=None, \*\*overrides)

Load publishing defaults from `config_file` (or the default location).

A missing file yields the built-in defaults. `overrides` (typically the
explicit keyword arguments a caller passed) take precedence when not
`None`, so call-site arguments always win over the file.

* **Return type:**
  [`YbConfig`](_autosummary/yb.config.html.md#yb.config.YbConfig)

### yb.prepare_and_publish(media, \*, language='English', language_code=None, audio_language_code=None, brand=None, extra_context=None, with_chapters=True, with_thumbnail=True, privacy_status=None, playlist=<object object>, category_id='28', config=None, client_secrets_file=None, token_file=None, progress=True, \*\*cred_kwargs)

One call: prepare publication content from `media` and upload it.

Transcribes (persisting the SRT next to the media) if needed, writes
LLM metadata, detects chapters, renders a thumbnail, then uploads with
captions + thumbnail + chapters and adds the video to the configured
playlist. `privacy_status` and `playlist` default to your `yb` config
(see [`publish_content()`](_autosummary/yb.html.md#yb.publish_content)).

`cred_kwargs` reach [`get_credentials()`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_credentials) (e.g.
`open_browser=False, port=8080` for headless consent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.prepare_content(media, , language='English', language_code=None, audio_language_code=None, brand=None, extra_context=None, transcript=None, with_chapters=True, with_thumbnail=False, thumbnail_text=None, metadata=None, model=None, transcribe_kwargs=None, chapters_kwargs=None)

Build a [`PublicationContent`](_autosummary/yb.html.md#yb.PublicationContent) for `media` (destination-agnostic).

Ensures a persisted SRT next to the media (transcribing once via
`mixing` if needed), writes LLM metadata, detects chapters (default on,
auto-skipped for clips too short to host them), and optionally renders a
thumbnail. Any precomputed pieces (`transcript`, `metadata`) are reused
instead of recomputed.

* **Parameters:**
  * **media** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Audio or video file.
  * **language** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Human-readable language to write metadata in.
  * **audio_language_code** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – BCP-47 codes for the metadata
    text and the spoken audio.
  * **brand** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Product/brand name to keep verbatim in the copy.
  * **extra_context** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Extra guidance for the copywriter (audience, CTA…).
  * **transcript** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – SRT text to use as-is (skips transcription).
  * **with_chapters** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Detect chapters (default `True`).
  * **with_thumbnail** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Render a thumbnail image (default `False`).
  * **thumbnail_text** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Overlay text for the thumbnail (defaults to the title).
  * **metadata** ([`ContentMetadata`](_autosummary/yb.content.html.md#yb.content.ContentMetadata) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Precomputed [`ContentMetadata`](_autosummary/yb.html.md#yb.ContentMetadata) to reuse.
  * **model** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – LLM model override.
  * **chapters_kwargs** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Forwarded to the mixing calls.
* **Return type:**
  [`PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent)
* **Returns:**
  A populated [`PublicationContent`](_autosummary/yb.html.md#yb.PublicationContent).

### yb.prepare_podcast_episode(media, output_dir, , content=None, audio=None, cover_image=None, make_cover_video=False, ken_burns=False, embed_chapters=True, language='English', brand=None, extra_context=None, audio_bitrate='192k')

Build a podcast episode bundle from `media` into `output_dir`.

* **Parameters:**
  * **media** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Source audio or video.
  * **output_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Directory to write the episode assets into.
  * **content** ([`PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Precomputed [`PublicationContent`](_autosummary/yb.html.md#yb.PublicationContent); built via
    [`yb.content.prepare_content()`](_autosummary/yb.content.html.md#yb.content.prepare_content) when omitted.
  * **audio** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Explicit episode audio. When omitted, `media` is used if it is
    audio, else its audio track is extracted to MP3.
  * **cover_image** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Cover art (required for `make_cover_video`).
  * **make_cover_video** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also render a cover-over-audio mp4 (e.g. for YouTube).
  * **ken_burns** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Apply a Ken Burns pan/zoom to the cover video.
  * **embed_chapters** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Embed ID3 chapter frames into the episode MP3.
  * **extra_context** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Forwarded to `prepare_content`.
  * **audio_bitrate** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Bitrate for extracted MP3 audio.
* **Return type:**
  [`PodcastEpisode`](_autosummary/yb.podcast.publish.html.md#yb.podcast.publish.PodcastEpisode)
* **Returns:**
  A `PodcastEpisode` with the asset paths.

### yb.publish_content(content, \*, privacy_status=None, playlist=<object object>, category_id='28', with_chapters=True, attach_caption=True, set_thumb=True, config=None, client_secrets_file=None, token_file=None, progress=True, service=None, resume=None, \*\*cred_kwargs)

Upload a prepared [`PublicationContent`](_autosummary/yb.html.md#yb.PublicationContent) to YouTube.

Maps the content to a YouTube snippet (chapters embedded in the
description when present), uploads, attaches the SRT caption track
(language = `content.audio_language` or `content.language`) and the
thumbnail when available, and adds the video to the configured playlist.

`privacy_status` and `playlist` default to your `yb` config (see
[`yb.config`](_autosummary/yb.config.html.md#module-yb.config)): unset means “use the config value”, so privacy falls back
to `unlisted` and the video joins your configured playlist. Pass
`playlist=None` to skip the playlist for one call, or `playlist="Name"`
to override the target.

`cred_kwargs` reach [`get_credentials()`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_credentials) (e.g.
`open_browser=False, port=8080` for headless consent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  `{"video_id", "url", "studio_url", "privacy_status", "captions",
  "thumbnail", "playlist"}`. `privacy_status` reflects what YouTube
  actually set (an unaudited project may force `"private"`); `playlist`
  is the [`add_video_to_playlist()`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.add_video_to_playlist) result or
  `None`.
* **Raises:**
  [**PartialPublish**](_autosummary/yb.youtube.html.md#yb.youtube.PartialPublish) – the upload succeeded and a later step failed;
      `err.result` holds the video id and what was done. Pass
      `resume=err.result` to finish without uploading again (yb#21).

### yb.set_chapters(video_id, chapters, , header='Chapters:', service=None, \*\*cred_kwargs)

Insert/replace a chapters block in the video’s description.

Strips any prior block under `header` and appends the new one. YouTube
renders interactive chapters when the first timestamp is `0:00`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.update_video_fields(video_id, , title=None, description=None, tags=None, category_id=None, default_language=None, default_audio_language=None, service=None, \*\*cred_kwargs)

Patch selected snippet fields, preserving the rest.

Fetches the current snippet, overlays the provided fields, and updates.
`categoryId` must be present (kept from the existing snippet if not given).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.upsert_caption(video_id, track, , language=None, name='', is_draft=False, replace=True, service=None, \*\*cred_kwargs)

Insert a caption track, or update the existing same-language one.

* **Parameters:**
  * **video_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Target video.
  * **track** ([`CaptionTrack`](_autosummary/yb.youtube.captions.html.md#yb.youtube.captions.CaptionTrack) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – A `CaptionTrack`, or a path (then `language` required).
  * **replace** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When an uploaded track in the same language exists, update it
    (`True`, default) rather than inserting a duplicate.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  The inserted/updated caption resource.

### yb.youtube_video_info(url, , extra_opts=None, cookies_file=None)

Fetch a video’s metadata without downloading it.

Returns the trimmed info dict (see `_INFO_FIELDS`). Useful to preview
the title/duration/chapters before deciding to download.

`cookies_file`: see [`download_youtube_video()`](_autosummary/yb.html.md#yb.download_youtube_video).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

### Modules

| [`audio_convert`](_autosummary/yb.audio_convert.html.md#module-yb.audio_convert)   | Convert audio files between formats with ffmpeg.                        |
|------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`config`](_autosummary/yb.config.html.md#module-yb.config)                 | User configuration for `yb` publishing defaults.                        |
| [`content`](_autosummary/yb.content.html.md#module-yb.content)               | Target-agnostic publication content: prepare it once, publish anywhere. |
| [`download`](_autosummary/yb.download.html.md#module-yb.download)             | Media download (yt-dlp) — fetch videos, audio, and their metadata.      |
| [`podcast`](_autosummary/yb.podcast.html.md#module-yb.podcast)               | Podcast publishing: show notes, chapters, cover video, and RSS.         |
| [`youtube`](_autosummary/yb.youtube.html.md#module-yb.youtube)               | YouTube publishing via the YouTube Data API v3.                         |


# _autosummary/yb.podcast.chapters.html.md

# yb.podcast.chapters

Podcast chapter markers: Podlove Simple Chapters (PSC) + MP3 ID3 chapters.

Two standards cover most podcast players:

- **PSC** — an XML sidecar (`<psc:chapters>`) referenced from the RSS feed.
- **ID3 chapters** — `CHAP`/`CTOC` frames embedded directly in the MP3, so
  chapters travel with the file (Apple Podcasts, Overcast, etc.).

Both are derived from the same platform-neutral `mixing.chapters.Chapter` list.

### Functions

| [`psc_xml`](_autosummary/yb.podcast.chapters.html.md#yb.podcast.chapters.psc_xml)(chapters)                                 | Render chapters as a Podlove Simple Chapters (PSC) XML document.   |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| [`write_id3_chapters`](_autosummary/yb.podcast.chapters.html.md#yb.podcast.chapters.write_id3_chapters)(mp3_path, chapters, \*[, ...]) | Embed ID3v2 chapter frames (`CHAP` + `CTOC`) into an MP3 file.     |
| [`write_psc`](_autosummary/yb.podcast.chapters.html.md#yb.podcast.chapters.write_psc)(chapters, path)                         | Write a PSC XML sidecar to `path`.                                 |

### yb.podcast.chapters.psc_xml(chapters)

Render chapters as a Podlove Simple Chapters (PSC) XML document.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.podcast.chapters.write_id3_chapters(mp3_path, chapters, , duration=None, toc_id='toc')

Embed ID3v2 chapter frames (`CHAP` + `CTOC`) into an MP3 file.

* **Parameters:**
  * **mp3_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – The MP3 to modify in place.
  * **chapters** ([`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[`Chapter`]) – Ordered chapter markers.
  * **duration** ([`float`](https://docs.python.org/3/builtins/functions.html#float) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Total media duration (s) — used as the last chapter’s end.
    Inferred from the file when omitted.
  * **toc_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Element id for the table-of-contents frame.
* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
* **Returns:**
  The path written.
* **Raises:**
  [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – `mutagen` is not installed (`pip install 'yb[podcast]'`).

### yb.podcast.chapters.write_psc(chapters, path)

Write a PSC XML sidecar to `path`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/yb.podcast.cover.html.md

# yb.podcast.cover

Cover-over-audio video: turn a podcast audio file into a simple video.

Useful for publishing an audio episode where a video is expected (YouTube): the
cover image, held for the episode’s duration, muxed with the audio — optionally
with a slow Ken Burns pan/zoom.

This is a thin adapter over `muvid.visualize`, which owns the audio→video
rendering. Reach for `muvid.visualize.render_audio_video()` directly when you
want a visual other than a held cover, a burnt-in title, or loudness
normalization. Needs `pip install 'yb[music]'` (pulls `muvid`).

### Functions

| [`cover_video`](_autosummary/yb.podcast.cover.html.md#yb.podcast.cover.cover_video)(audio, image, \*[, saveas, ...])   | Render a video of `image` held over `audio`.   |
|-------------------------------------------------------------------------------------------------|------------------------------------------------|

### yb.podcast.cover.cover_video(audio, image, , saveas=None, ken_burns=False, size=None, fps=None, layout=None)

Render a video of `image` held over `audio`.

The cover is composed onto a 16:9 canvas — filled with a blurred, darkened
copy of itself rather than black bars — so square or portrait art still
looks deliberate at 1080p.

* **Parameters:**
  * **audio** (PathLike) – The episode audio file.
  * **image** (PathLike) – Cover image to display.
  * **saveas** (PathLike | None) – Output path (defaults to `<audio-stem>.cover.mp4`).
  * **ken_burns** (bool) – Apply a slow pan/zoom (rendered by `burns`) instead of a
    static image. Much slower than the static path.
  * **size** (tuple[int, int] | None) – Output resolution (width, height); defaults to 1080p.
  * **fps** (int | None) – Output frame rate; defaults to muvid’s default.
  * **layout** (CoverLayout | None) – How the cover sits on the canvas.
* **Return type:**
  Path
* **Returns:**
  Path to the rendered mp4.
* **Raises:**
  * [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – the `muvid` package is not installed (`pip install
        'yb[music]'`).
  * **FfmpegError** – ffmpeg is missing, or the render failed.


# _autosummary/yb.podcast.feed.html.md

# yb.podcast.feed

Podcast RSS feed generation (iTunes-compatible) via feedgen.

Builds a standards-compliant podcast RSS channel from a channel description and
a list of episodes. Chapters travel with the audio (ID3 frames) and/or as a PSC
sidecar; a Podcasting-2.0 `<podcast:chapters>` URL can be supplied per episode.

`pubdate` is always caller-supplied (never implicitly “now”) to keep feed
generation deterministic and reproducible.

### Functions

| [`build_feed`](_autosummary/yb.podcast.feed.html.md#yb.podcast.feed.build_feed)(channel, episodes)   | Build podcast RSS XML for `channel` and its `episodes`.   |
|----------------------------------------------------------------------------------|-----------------------------------------------------------|

### Classes

| [`EpisodeFeedItem`](_autosummary/yb.podcast.feed.html.md#yb.podcast.feed.EpisodeFeedItem)(title, description, ...[, ...])   | One episode's feed entry.      |
|----------------------------------------------------------------------------------------------------|--------------------------------|
| [`PodcastChannel`](_autosummary/yb.podcast.feed.html.md#yb.podcast.feed.PodcastChannel)(title, link, description, ...)     | Podcast-level (show) metadata. |

### *class* yb.podcast.feed.EpisodeFeedItem(title, description, audio_url, audio_length_bytes, pubdate, guid=None, audio_mime='audio/mpeg', duration_seconds=None, image_url=None, chapters_url=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One episode’s feed entry.

### *class* yb.podcast.feed.PodcastChannel(title, link, description, author, email, image_url=None, language='en', categories=<factory>, explicit=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Podcast-level (show) metadata.

### yb.podcast.feed.build_feed(channel, episodes)

Build podcast RSS XML for `channel` and its `episodes`.

* **Raises:**
  [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – `feedgen` is not installed (`pip install 'yb[podcast]'`).
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/yb.podcast.html.md

# yb.podcast

Podcast publishing: show notes, chapters, cover video, and RSS.

Optionally installed (`pip install 'yb[podcast]'`). Consumes the same
platform-neutral [`yb.content.PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent) as the YouTube adapter.

```pycon
>>> from yb.podcast import prepare_podcast_episode
>>> ep = prepare_podcast_episode("episode.mp3", "out/", cover_image="cover.jpg")
>>> print(ep.show_notes, ep.chapters_psc)
```

### Functions

| [`format_show_notes`](_autosummary/yb.podcast.html.md#yb.podcast.format_show_notes)(content, \*[, include_chapters])   | Render show notes (title + description + optional chapters) as text.   |
|-------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| [`psc_xml`](_autosummary/yb.podcast.html.md#yb.podcast.psc_xml)(chapters)                                    | Render chapters as a Podlove Simple Chapters (PSC) XML document.       |
| [`write_psc`](_autosummary/yb.podcast.html.md#yb.podcast.write_psc)(chapters, path)                            | Write a PSC XML sidecar to `path`.                                     |
| [`write_id3_chapters`](_autosummary/yb.podcast.html.md#yb.podcast.write_id3_chapters)(mp3_path, chapters, \*[, ...])    | Embed ID3v2 chapter frames (`CHAP` + `CTOC`) into an MP3 file.         |
| [`cover_video`](_autosummary/yb.podcast.html.md#yb.podcast.cover_video)(audio, image, \*[, saveas, ...])         | Render a video of `image` held over `audio`.                           |
| [`build_feed`](_autosummary/yb.podcast.html.md#yb.podcast.build_feed)(channel, episodes)                        | Build podcast RSS XML for `channel` and its `episodes`.                |
| [`prepare_podcast_episode`](_autosummary/yb.podcast.html.md#yb.podcast.prepare_podcast_episode)(media, output_dir, \*)       | Build a podcast episode bundle from `media` into `output_dir`.         |

### Classes

| [`PodcastChannel`](_autosummary/yb.podcast.html.md#yb.podcast.PodcastChannel)(title, link, description, ...)   | Podcast-level (show) metadata.         |
|--------------------------------------------------------------------------------------------------|----------------------------------------|
| [`EpisodeFeedItem`](_autosummary/yb.podcast.html.md#yb.podcast.EpisodeFeedItem)(title, description, ...[, ...]) | One episode's feed entry.              |
| [`PodcastEpisode`](_autosummary/yb.podcast.html.md#yb.podcast.PodcastEpisode)(audio, show_notes[, ...])        | Paths to the generated episode assets. |

### *class* yb.podcast.EpisodeFeedItem(title, description, audio_url, audio_length_bytes, pubdate, guid=None, audio_mime='audio/mpeg', duration_seconds=None, image_url=None, chapters_url=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One episode’s feed entry.

### *class* yb.podcast.PodcastChannel(title, link, description, author, email, image_url=None, language='en', categories=<factory>, explicit=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Podcast-level (show) metadata.

### *class* yb.podcast.PodcastEpisode(audio, show_notes, chapters_psc=None, chapters_json=None, cover_video=None, content=None, extras=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Paths to the generated episode assets.

### yb.podcast.build_feed(channel, episodes)

Build podcast RSS XML for `channel` and its `episodes`.

* **Raises:**
  [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – `feedgen` is not installed (`pip install 'yb[podcast]'`).
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.podcast.cover_video(audio, image, , saveas=None, ken_burns=False, size=None, fps=None, layout=None)

Render a video of `image` held over `audio`.

The cover is composed onto a 16:9 canvas — filled with a blurred, darkened
copy of itself rather than black bars — so square or portrait art still
looks deliberate at 1080p.

* **Parameters:**
  * **audio** (PathLike) – The episode audio file.
  * **image** (PathLike) – Cover image to display.
  * **saveas** (PathLike | None) – Output path (defaults to `<audio-stem>.cover.mp4`).
  * **ken_burns** (bool) – Apply a slow pan/zoom (rendered by `burns`) instead of a
    static image. Much slower than the static path.
  * **size** (tuple[int, int] | None) – Output resolution (width, height); defaults to 1080p.
  * **fps** (int | None) – Output frame rate; defaults to muvid’s default.
  * **layout** (CoverLayout | None) – How the cover sits on the canvas.
* **Return type:**
  Path
* **Returns:**
  Path to the rendered mp4.
* **Raises:**
  * [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – the `muvid` package is not installed (`pip install
        'yb[music]'`).
  * **FfmpegError** – ffmpeg is missing, or the render failed.

### yb.podcast.format_show_notes(content, , include_chapters=True)

Render show notes (title + description + optional chapters) as text.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.podcast.prepare_podcast_episode(media, output_dir, , content=None, audio=None, cover_image=None, make_cover_video=False, ken_burns=False, embed_chapters=True, language='English', brand=None, extra_context=None, audio_bitrate='192k')

Build a podcast episode bundle from `media` into `output_dir`.

* **Parameters:**
  * **media** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Source audio or video.
  * **output_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Directory to write the episode assets into.
  * **content** ([`PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Precomputed `PublicationContent`; built via
    [`yb.content.prepare_content()`](_autosummary/yb.content.html.md#yb.content.prepare_content) when omitted.
  * **audio** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Explicit episode audio. When omitted, `media` is used if it is
    audio, else its audio track is extracted to MP3.
  * **cover_image** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Cover art (required for `make_cover_video`).
  * **make_cover_video** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also render a cover-over-audio mp4 (e.g. for YouTube).
  * **ken_burns** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Apply a Ken Burns pan/zoom to the cover video.
  * **embed_chapters** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Embed ID3 chapter frames into the episode MP3.
  * **extra_context** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Forwarded to `prepare_content`.
  * **audio_bitrate** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Bitrate for extracted MP3 audio.
* **Return type:**
  [`PodcastEpisode`](_autosummary/yb.podcast.publish.html.md#yb.podcast.publish.PodcastEpisode)
* **Returns:**
  A [`PodcastEpisode`](_autosummary/yb.podcast.html.md#yb.podcast.PodcastEpisode) with the asset paths.

### yb.podcast.psc_xml(chapters)

Render chapters as a Podlove Simple Chapters (PSC) XML document.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.podcast.write_id3_chapters(mp3_path, chapters, , duration=None, toc_id='toc')

Embed ID3v2 chapter frames (`CHAP` + `CTOC`) into an MP3 file.

* **Parameters:**
  * **mp3_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – The MP3 to modify in place.
  * **chapters** ([`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[`Chapter`]) – Ordered chapter markers.
  * **duration** ([`float`](https://docs.python.org/3/builtins/functions.html#float) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Total media duration (s) — used as the last chapter’s end.
    Inferred from the file when omitted.
  * **toc_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Element id for the table-of-contents frame.
* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
* **Returns:**
  The path written.
* **Raises:**
  [**ImportError**](https://docs.python.org/3/builtins/exceptions.html#ImportError) – `mutagen` is not installed (`pip install 'yb[podcast]'`).

### yb.podcast.write_psc(chapters, path)

Write a PSC XML sidecar to `path`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### Modules

| [`chapters`](_autosummary/yb.podcast.chapters.html.md#module-yb.podcast.chapters)   | Podcast chapter markers: Podlove Simple Chapters (PSC) + MP3 ID3 chapters.   |
|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`cover`](_autosummary/yb.podcast.cover.html.md#module-yb.podcast.cover)         | Cover-over-audio video: turn a podcast audio file into a simple video.       |
| [`feed`](_autosummary/yb.podcast.feed.html.md#module-yb.podcast.feed)           | Podcast RSS feed generation (iTunes-compatible) via feedgen.                 |
| [`publish`](_autosummary/yb.podcast.publish.html.md#module-yb.podcast.publish)     | Assemble a publishable podcast episode from a media file.                    |
| [`shownotes`](_autosummary/yb.podcast.shownotes.html.md#module-yb.podcast.shownotes) | Podcast show notes — the human-readable episode description block.           |


# _autosummary/yb.podcast.publish.html.md

# yb.podcast.publish

Assemble a publishable podcast episode from a media file.

Produces the per-episode asset bundle in an output directory: an MP3 (with
ID3 chapters embedded), show-notes text, a PSC chapter sidecar, a chapters
JSON, and optionally a cover-over-audio video for video platforms. Delivery to
a host/Spotify is via RSS ([`yb.podcast.feed`](_autosummary/yb.podcast.feed.html.md#module-yb.podcast.feed)) — this prepares the assets
and a ready-to-use [`yb.podcast.feed.EpisodeFeedItem`](_autosummary/yb.podcast.feed.html.md#yb.podcast.feed.EpisodeFeedItem) stub.

### Functions

| [`prepare_podcast_episode`](_autosummary/yb.podcast.publish.html.md#yb.podcast.publish.prepare_podcast_episode)(media, output_dir, \*)   | Build a podcast episode bundle from `media` into `output_dir`.   |
|---------------------------------------------------------------------------------------------------|------------------------------------------------------------------|

### Classes

| [`PodcastEpisode`](_autosummary/yb.podcast.publish.html.md#yb.podcast.publish.PodcastEpisode)(audio, show_notes[, ...])   | Paths to the generated episode assets.   |
|---------------------------------------------------------------------------------------------|------------------------------------------|

### *class* yb.podcast.publish.PodcastEpisode(audio, show_notes, chapters_psc=None, chapters_json=None, cover_video=None, content=None, extras=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Paths to the generated episode assets.

### yb.podcast.publish.prepare_podcast_episode(media, output_dir, , content=None, audio=None, cover_image=None, make_cover_video=False, ken_burns=False, embed_chapters=True, language='English', brand=None, extra_context=None, audio_bitrate='192k')

Build a podcast episode bundle from `media` into `output_dir`.

* **Parameters:**
  * **media** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Source audio or video.
  * **output_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – Directory to write the episode assets into.
  * **content** ([`PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Precomputed `PublicationContent`; built via
    [`yb.content.prepare_content()`](_autosummary/yb.content.html.md#yb.content.prepare_content) when omitted.
  * **audio** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Explicit episode audio. When omitted, `media` is used if it is
    audio, else its audio track is extracted to MP3.
  * **cover_image** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Cover art (required for `make_cover_video`).
  * **make_cover_video** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Also render a cover-over-audio mp4 (e.g. for YouTube).
  * **ken_burns** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Apply a Ken Burns pan/zoom to the cover video.
  * **embed_chapters** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Embed ID3 chapter frames into the episode MP3.
  * **extra_context** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Forwarded to `prepare_content`.
  * **audio_bitrate** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Bitrate for extracted MP3 audio.
* **Return type:**
  [`PodcastEpisode`](_autosummary/yb.podcast.publish.html.md#yb.podcast.publish.PodcastEpisode)
* **Returns:**
  A [`PodcastEpisode`](_autosummary/yb.podcast.publish.html.md#yb.podcast.publish.PodcastEpisode) with the asset paths.


# _autosummary/yb.podcast.shownotes.html.md

# yb.podcast.shownotes

Podcast show notes — the human-readable episode description block.

Same content as a YouTube description, formatted for podcast directories:
title, body, then a chapters list (`M:SS Title`) when chapters are present.

### Functions

| [`format_show_notes`](_autosummary/yb.podcast.shownotes.html.md#yb.podcast.shownotes.format_show_notes)(content, \*[, include_chapters])   | Render show notes (title + description + optional chapters) as text.   |
|-------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|

### yb.podcast.shownotes.format_show_notes(content, , include_chapters=True)

Render show notes (title + description + optional chapters) as text.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/yb.youtube.api.html.md

# yb.youtube.api

YouTube video operations: get, upload, update metadata, thumbnail, chapters.

These wrap the YouTube Data API v3 `videos`/`thumbnails` resources for both
creating videos and *editing already-published ones* (titles, descriptions,
tags, languages, and chapter blocks in the description).

### Module Attributes

| [`PRIVACY_STATUSES`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.PRIVACY_STATUSES)   | Privacy values `videos.update(part="status")` accepts, least visible first.   |
|---------------------------------------------------------------------|-------------------------------------------------------------------------------|

### Functions

| [`get_video`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.get_video)(video_id, \*[, part, service])           | Fetch a video resource (raises `KeyError` if not found/visible).      |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`set_chapters`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.set_chapters)(video_id, chapters, \*[, ...])        | Insert/replace a chapters block in the video's description.           |
| [`set_thumbnail`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.set_thumbnail)(video_id, image_path, \*[, service]) | Set a custom thumbnail (`thumbnails.set`).                            |
| [`supersede`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.supersede)(old_id, new_id, \*[, old_privacy, ...])  | Retire an upload that `new_id` replaces (yb#19).                      |
| [`update_video`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.update_video)(video_id, snippet, \*[, service])     | Update a video's snippet (`videos.update`).                           |
| [`update_video_fields`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.update_video_fields)(video_id, \*[, title, ...])    | Patch selected snippet fields, preserving the rest.                   |
| [`update_video_status`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.update_video_status)(video_id, \*[, ...])           | Change a video's privacy (or synthetic-media flag), keeping the rest. |
| [`upload_video`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.upload_video)(video_path, body, \*[, service, ...]) | Resumably upload a video with the given `videos.insert` `body`.       |

### Exceptions

| [`VisibilityDropWarning`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.VisibilityDropWarning)   | [`supersede()`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.supersede) leaves the re-cut less visible than the original was.   |
|--------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|

### yb.youtube.api.PRIVACY_STATUSES *= ('private', 'unlisted', 'public')*

Privacy values `videos.update(part="status")` accepts, least visible first.

### *exception* yb.youtube.api.VisibilityDropWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

[`supersede()`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.supersede) leaves the re-cut less visible than the original was.

Emitted before anything is changed, so escalating it refuses the call:

```default
warnings.simplefilter("error", VisibilityDropWarning)
```

### yb.youtube.api.get_video(video_id, , part='snippet,status', service=None, \*\*cred_kwargs)

Fetch a video resource (raises `KeyError` if not found/visible).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.api.set_chapters(video_id, chapters, , header='Chapters:', service=None, \*\*cred_kwargs)

Insert/replace a chapters block in the video’s description.

Strips any prior block under `header` and appends the new one. YouTube
renders interactive chapters when the first timestamp is `0:00`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.api.set_thumbnail(video_id, image_path, , service=None, \*\*cred_kwargs)

Set a custom thumbnail (`thumbnails.set`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.api.supersede(old_id, new_id, , old_privacy='private', new_privacy=None, move_playlists=True, service=None, \*\*cred_kwargs)

Retire an upload that `new_id` replaces (yb#19).

YouTube cannot replace a video’s file, so a re-cut is a new upload. This
puts the new one where the old one was and takes the old one out of view:

1. refuses, before changing anything, unless `new_id` has finished
   processing (`uploadStatus == "processed"`): retiring the old video for
   a failed or rejected upload would leave the public neither;
2. sets `new_id` to `new_privacy` when given (e.g. `"public"`);
3. for every playlist of yours that holds `old_id` (when
   `move_playlists`): inserts `new_id` at the old one’s position (or
   appends, if the playlist is not sorted manually), THEN removes
   `old_id`, so a failure part-way never leaves a playlist with neither;
4. sets `old_id` to `old_privacy` (default `"private"`), clearing any
   scheduled publish. It is never deleted.

The new video keeps its own privacy unless `new_privacy` is given, and an
upload is `unlisted` by default: so the re-cut of a public video is NOT
made public for you. When the result is less visible than the old video
was (a public original, an unlisted re-cut), a [`VisibilityDropWarning`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.VisibilityDropWarning)
is emitted before anything changes; pass `new_privacy="public"` to publish
the re-cut, or turn the warning into an error to refuse such calls.

Captions and thumbnails are not copied: upload them with the new video
([`yb.youtube.publish_video()`](_autosummary/yb.youtube.html.md#yb.youtube.publish_video)). Safe to re-run: every step checks
before it acts. Quota: see [`yb.youtube.playlists_containing()`](_autosummary/yb.youtube.html.md#yb.youtube.playlists_containing); each
write (insert, delete, update) is 50 units.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  `{"old_id", "new_id", "playlists", "old_privacy", "new_privacy"}`
  where `playlists` lists `{"playlist_id", "added_new", "position",
  "removed_old"}`.

### yb.youtube.api.update_video(video_id, snippet, , service=None, \*\*cred_kwargs)

Update a video’s snippet (`videos.update`). `categoryId` is required.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.api.update_video_fields(video_id, , title=None, description=None, tags=None, category_id=None, default_language=None, default_audio_language=None, service=None, \*\*cred_kwargs)

Patch selected snippet fields, preserving the rest.

Fetches the current snippet, overlays the provided fields, and updates.
`categoryId` must be present (kept from the existing snippet if not given).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.api.update_video_status(video_id, , privacy_status=None, contains_synthetic_media=None, keep_schedule=False, service=None, \*\*cred_kwargs)

Change a video’s privacy (or synthetic-media flag), keeping the rest.

Read-modify-write of the `status` part (yb#19): `videos.update` replaces
the part wholesale, so the current writable fields are carried over and
only the ones given change. Making a video private never deletes it.

Setting `privacy_status` clears a scheduled publish (`publishAt`):
otherwise a video made private while scheduled would still go public at
that time, and a past `publishAt` read back is refused by the API. Pass
`keep_schedule=True` to keep it (only meaningful with `"private"`).

* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – `privacy_status` is not one of [`PRIVACY_STATUSES`](_autosummary/yb.youtube.api.html.md#yb.youtube.api.PRIVACY_STATUSES).
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.api.upload_video(video_path, body, , service=None, chunksize=8388608, progress=True, \*\*cred_kwargs)

Resumably upload a video with the given `videos.insert` `body`.

Returns the created video resource (includes `id`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/yb.youtube.auth.html.md

# yb.youtube.auth

OAuth 2.0 plumbing for the YouTube Data API v3.

Runs the installed-app consent flow once (browser), caches the token, and
refreshes it silently thereafter. Needs only `google-api-python-client` +
`google-auth-oauthlib` (`pip install 'yb[youtube]'`) and an OAuth client of
type *Desktop app* — point `client_secrets_file` at its JSON or set
`$YOUTUBE_CLIENT_SECRETS_FILE` / `$GOOGLE_CLIENT_SECRETS_FILE`.

No `gcloud` required: creating the project / enabling the API / making the
OAuth client is all doable in the Google Cloud console (the `yb-setup` skill
walks through it).

### Module Attributes

| [`DEFAULT_SCOPES`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.DEFAULT_SCOPES)            | Upload + force-ssl (the latter is needed for captions.insert and thumbnails.set).                                                                                                                                                |
|----------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`CONSENT_MODES`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.CONSENT_MODES)             | `"local"` waits for a redirect on this machine's `localhost`; `"paste"` is two-step and needs no shared network (see [`yb.youtube.paste_consent`](_autosummary/yb.youtube.paste_consent.html.md#module-yb.youtube.paste_consent)). |
| [`DEFAULT_CONSENT_TIMEOUT_S`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.DEFAULT_CONSENT_TIMEOUT_S) | How long consent may wait for its redirect before giving up.                                                                                                                                                                     |

### Functions

| [`default_client_secrets_file`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.default_client_secrets_file)()                   | Default OAuth client-secrets location: `<config dir>/client_secret.json`.   |
|--------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`default_token_file`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.default_token_file)()                            | Cached OAuth token location (`$XDG_CONFIG_HOME` or `~/.config`).            |
| [`get_credentials`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_credentials)(\*[, client_secrets_file, ...]) | Return OAuth user credentials, running the consent flow if needed.          |
| [`get_service`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_service)(\*[, credentials])                  | Build a YouTube Data API v3 service object.                                 |

### Exceptions

| [`ConsentRequired`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.ConsentRequired)   | Consent is needed and this process cannot obtain it.   |
|--------------------------------------------------------------------|--------------------------------------------------------|

### yb.youtube.auth.CONSENT_MODES *= ('local', 'paste')*

`"local"` waits for a redirect on this machine’s
`localhost`; `"paste"` is two-step and needs no shared network (see
[`yb.youtube.paste_consent`](_autosummary/yb.youtube.paste_consent.html.md#module-yb.youtube.paste_consent)).

* **Type:**
  Ways to obtain consent

### *exception* yb.youtube.auth.ConsentRequired

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Consent is needed and this process cannot obtain it.

Raised instead of starting a consent flow that nobody can complete. The
alternative is worse than an error: `run_local_server` prints a URL to a
stdout nobody is reading and waits on `localhost` for a redirect that will
never arrive, so an automated caller *blocks* rather than failing. That has
happened — an upload stopped dead mid-script and read as a slow network for
hours, when the underlying cause was one line of `invalid_grant`.

### yb.youtube.auth.DEFAULT_CONSENT_TIMEOUT_S *= 300.0*

How long consent may wait for its redirect before giving up.

The library’s own default is “wait indefinitely”, which is not a good default
for anything: a caller that cannot complete consent then *blocks* rather than
failing, and an automated upload reads as a slow network until someone thinks
to run `ps`. Five minutes is long enough for a human to find the tab and
click through an unverified-app warning, and short enough that a machine
finds out today. Pass `timeout_seconds=None` for the old behaviour.

### yb.youtube.auth.DEFAULT_SCOPES *= ['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl']*

Upload + force-ssl (the latter is needed for captions.insert and thumbnails.set).

### yb.youtube.auth.default_client_secrets_file()

Default OAuth client-secrets location: `<config dir>/client_secret.json`.

Used when neither `client_secrets_file=` nor the env vars are set, so all
of `yb`’s state can live in one directory next to the token.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### yb.youtube.auth.default_token_file()

Cached OAuth token location (`$XDG_CONFIG_HOME` or `~/.config`).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### yb.youtube.auth.get_credentials(, client_secrets_file=None, token_file=None, scopes=['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl'], open_browser=True, port=0, timeout_seconds=300.0, interactive=True, consent='local', authorization_response=None)

Return OAuth user credentials, running the consent flow if needed.

First use runs the installed-app consent flow and caches the token to
`token_file` so later calls are non-interactive. Expired tokens are
refreshed automatically.

Pass `interactive=False` when nobody can answer a consent prompt — a cron
job, a queue worker, an agent. Consent is then never started; a token that
cannot be refreshed raises [`ConsentRequired`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.ConsentRequired) naming the cause and the
fix. This is not an edge case: an OAuth client left in “Testing” rotates
refresh tokens out after about seven days, so anything scheduled hits it
weekly.

Even with the default `interactive=True` the call can no longer hang:
consent is bounded by `timeout_seconds`
([`DEFAULT_CONSENT_TIMEOUT_S`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.DEFAULT_CONSENT_TIMEOUT_S)), and a timeout is reported as the same
[`ConsentRequired`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.ConsentRequired), because “nobody answered” and “nobody could
answer” want the same thing done about them.

Consent always goes through a temporary local web server on `port` (`0`
picks a free one), because Google retired the copy-paste “out-of-band” flow
in 2022. `open_browser=False` only stops the browser from being launched:
the authorization URL is printed instead, and the redirect must still reach
that local server.

Headless recipe: pass `interactive=True` (there is no terminal, so the
default would refuse), `open_browser=False` and a fixed `port=`, then
forward that port from the machine holding the browser
(`ssh -L <port>:localhost:<port> <host>`). Nothing needs registering in
the Cloud console — the *Desktop app* client this module requires accepts
any `localhost` port, which is also why the `port=0` default works.

The call blocks until the redirect arrives, bounded by `timeout_seconds`
(default [`DEFAULT_CONSENT_TIMEOUT_S`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.DEFAULT_CONSENT_TIMEOUT_S); pass `None` for the library’s
“wait indefinitely”, which is what this used to do).

\*\*No shared `localhost`? Use\*\* `consent="paste"`. Consent then needs no
running server and no waiting: the first call returns by raising
[`ConsentPending`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.ConsentPending) carrying the URL to open
(anywhere — a phone will do); the browser’s redirect to `localhost` fails
to load, and the second call passes that address-bar URL (or just its
`code`) as `authorization_response=` to finish and cache the token. The
two calls may be separate turns or separate processes; `yb auth` is the
command-line form. `port` only names the (unreachable) redirect.

These keywords ride `**cred_kwargs` through [`get_service()`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_service) and the
publishing helpers. The few entry points that take none (notably
`yb.music.publish.publish_folder()`) still work headlessly: call this
once to mint the token, after which nothing prompts again.

### yb.youtube.auth.get_service(, credentials=None, \*\*cred_kwargs)

Build a YouTube Data API v3 service object.

`cred_kwargs` are forwarded to [`get_credentials()`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_credentials) (`token_file=`,
`open_browser=`, `port=`, …) unless `credentials` is given.


# _autosummary/yb.youtube.captions.html.md

# yb.youtube.captions

Caption (subtitle) tracks on YouTube videos: list, insert, update, upsert.

Lets you attach an SRT/VTT track at upload time *and* manage tracks on
already-published videos — e.g. replace a hand-edited subtitle file, or add a
track in a new language. `upsert_caption` is the convenient default: it
updates an existing same-language track if present, else inserts a new one.

#### NOTE
YouTube auto-generates ASR tracks of its own (`trackKind="asr"`);
these helpers operate on uploaded `"standard"` tracks and ignore ASR.

### Functions

| [`delete_caption`](_autosummary/yb.youtube.captions.html.md#yb.youtube.captions.delete_caption)(caption_id, \*[, service])         | Delete a caption track (`captions.delete`).                                              |
|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| [`download_caption`](_autosummary/yb.youtube.captions.html.md#yb.youtube.captions.download_caption)(caption_id, \*[, tfmt, service]) | Download a caption track's content (`captions.download`), default SRT.                   |
| [`insert_caption`](_autosummary/yb.youtube.captions.html.md#yb.youtube.captions.insert_caption)(video_id, path, \*, language)      | Insert a new caption track (`captions.insert`).                                          |
| [`list_captions`](_autosummary/yb.youtube.captions.html.md#yb.youtube.captions.list_captions)(video_id, \*[, service])            | List caption tracks on a video (each item's snippet has language/name/trackKind/status). |
| [`update_caption`](_autosummary/yb.youtube.captions.html.md#yb.youtube.captions.update_caption)(caption_id, path, \*[, ...])       | Replace the content of an existing caption track (`captions.update`).                    |
| [`upsert_caption`](_autosummary/yb.youtube.captions.html.md#yb.youtube.captions.upsert_caption)(video_id, track, \*[, ...])        | Insert a caption track, or update the existing same-language one.                        |

### Classes

| [`CaptionTrack`](_autosummary/yb.youtube.captions.html.md#yb.youtube.captions.CaptionTrack)(path, language[, name, is_draft])   | A caption track to attach to a video.   |
|---------------------------------------------------------------------------------------------------|-----------------------------------------|

### *class* yb.youtube.captions.CaptionTrack(path, language, name='', is_draft=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A caption track to attach to a video.

#### path

Path to an `.srt` (or `.vtt`) file.

#### language

BCP-47 language code, e.g. `"en"` or `"fr"`.

#### name

Track name shown in the YouTube UI (e.g. `"English"`).

#### is_draft

When `True`, uploaded but not published to viewers.

### yb.youtube.captions.delete_caption(caption_id, , service=None, \*\*cred_kwargs)

Delete a caption track (`captions.delete`).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.youtube.captions.download_caption(caption_id, , tfmt='srt', service=None, \*\*cred_kwargs)

Download a caption track’s content (`captions.download`), default SRT.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### yb.youtube.captions.insert_caption(video_id, path, , language, name='', is_draft=False, service=None, \*\*cred_kwargs)

Insert a new caption track (`captions.insert`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.captions.list_captions(video_id, , service=None, \*\*cred_kwargs)

List caption tracks on a video (each item’s snippet has language/name/trackKind/status).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### yb.youtube.captions.update_caption(caption_id, path, , is_draft=None, service=None, \*\*cred_kwargs)

Replace the content of an existing caption track (`captions.update`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.captions.upsert_caption(video_id, track, , language=None, name='', is_draft=False, replace=True, service=None, \*\*cred_kwargs)

Insert a caption track, or update the existing same-language one.

* **Parameters:**
  * **video_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Target video.
  * **track** ([`CaptionTrack`](_autosummary/yb.youtube.captions.html.md#yb.youtube.captions.CaptionTrack) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – A [`CaptionTrack`](_autosummary/yb.youtube.captions.html.md#yb.youtube.captions.CaptionTrack), or a path (then `language` required).
  * **replace** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When an uploaded track in the same language exists, update it
    (`True`, default) rather than inserting a duplicate.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  The inserted/updated caption resource.


# _autosummary/yb.youtube.html.md

# yb.youtube

YouTube publishing via the YouTube Data API v3.

Optionally installed (`pip install 'yb[youtube]'`). Needs an OAuth Desktop
client (see the `yb-setup` skill) — set `$YOUTUBE_CLIENT_SECRETS_FILE`.

Create:

```pycon
>>> from yb.youtube import prepare_and_publish
>>> prepare_and_publish("promo.fr.mp4", language="French", language_code="fr",
...                     audio_language_code="fr", privacy_status="unlisted")
```

Edit an existing video:

```pycon
>>> from yb.youtube import update_video_fields, upsert_caption, set_chapters
>>> update_video_fields("VIDEO_ID", title="New title")
>>> upsert_caption("VIDEO_ID", "subs.fr.srt", language="fr", name="Français")
```

### Functions

| [`api_available`](_autosummary/yb.youtube.html.md#yb.youtube.api_available)(\*[, api_key])                       | Whether the Data API can actually be called with the key we have.                                                       |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| `watch_url`(video_id)                                                                               |                                                                                                                         |
| [`thumbnail_url`](_autosummary/yb.youtube.html.md#yb.youtube.thumbnail_url)(video_id, \*[, quality])             | Thumbnail URL for a video.                                                                                              |
| [`rank_by_engagement`](_autosummary/yb.youtube.html.md#yb.youtube.rank_by_engagement)(videos, \*[, min_views, ...])   | Order videos by a like-rate-and-reach score.                                                                            |
| [`add_stats`](_autosummary/yb.youtube.html.md#yb.youtube.add_stats)(videos, \*[, api_key, backend])          | Fill in engagement numbers on search results, in place-ish (returns them).                                              |
| [`search_videos`](_autosummary/yb.youtube.html.md#yb.youtube.search_videos)(query, \*[, max_results, ...])       | Search YouTube and return a list of video records.                                                                      |
| [`start_paste_consent`](_autosummary/yb.youtube.html.md#yb.youtube.start_paste_consent)(\*[, ...])                     | Begin paste-back consent and return the URL to open.                                                                    |
| [`finish_paste_consent`](_autosummary/yb.youtube.html.md#yb.youtube.finish_paste_consent)(authorization_response, \*)   | Complete a consent begun by [`start_paste_consent()`](_autosummary/yb.youtube.html.md#yb.youtube.start_paste_consent); return credentials. |
| [`get_credentials`](_autosummary/yb.youtube.html.md#yb.youtube.get_credentials)(\*[, client_secrets_file, ...])    | Return OAuth user credentials, running the consent flow if needed.                                                      |
| [`get_service`](_autosummary/yb.youtube.html.md#yb.youtube.get_service)(\*[, credentials])                     | Build a YouTube Data API v3 service object.                                                                             |
| [`default_token_file`](_autosummary/yb.youtube.html.md#yb.youtube.default_token_file)()                               | Cached OAuth token location (`$XDG_CONFIG_HOME` or `~/.config`).                                                        |
| [`default_client_secrets_file`](_autosummary/yb.youtube.html.md#yb.youtube.default_client_secrets_file)()                      | Default OAuth client-secrets location: `<config dir>/client_secret.json`.                                               |
| [`list_my_playlists`](_autosummary/yb.youtube.html.md#yb.youtube.list_my_playlists)(\*[, service])                   | Return all playlists owned by the authenticated channel (paginated).                                                    |
| [`find_playlist`](_autosummary/yb.youtube.html.md#yb.youtube.find_playlist)(title, \*[, service])                | Return the id of the caller's playlist titled `title` (or `None`).                                                      |
| [`create_playlist`](_autosummary/yb.youtube.html.md#yb.youtube.create_playlist)(title, \*[, description, ...])     | Create a playlist and return its id.                                                                                    |
| [`ensure_playlist`](_autosummary/yb.youtube.html.md#yb.youtube.ensure_playlist)(title, \*[, create, ...])          | Return the id of the playlist titled `title`, creating it if missing.                                                   |
| [`is_video_in_playlist`](_autosummary/yb.youtube.html.md#yb.youtube.is_video_in_playlist)(video_id, playlist_id, \*)    | Whether `video_id` is already an item of `playlist_id` (paginated).                                                     |
| [`add_to_playlist`](_autosummary/yb.youtube.html.md#yb.youtube.add_to_playlist)(video_id, playlist_id, \*[, ...])  | Add `video_id` to `playlist_id` (`playlistItems.insert`).                                                               |
| [`add_video_to_playlist`](_autosummary/yb.youtube.html.md#yb.youtube.add_video_to_playlist)(video_id, title, \*[, ...])  | Find-or-create the playlist named `title` and append `video_id`.                                                        |
| [`playlist_item_ids`](_autosummary/yb.youtube.html.md#yb.youtube.playlist_item_ids)(video_id, playlist_id, \*)       | The `playlistItems` ids holding `video_id` in `playlist_id`.                                                            |
| [`first_position`](_autosummary/yb.youtube.html.md#yb.youtube.first_position)(video_id, playlist_id, \*[, ...])   | The 0-based position of `video_id`'s first occurrence (`None` if absent).                                               |
| [`playlists_containing`](_autosummary/yb.youtube.html.md#yb.youtube.playlists_containing)(video_id, \*[, service])      | Ids of the authenticated channel's playlists that hold `video_id`.                                                      |
| [`remove_from_playlist`](_autosummary/yb.youtube.html.md#yb.youtube.remove_from_playlist)(video_id, playlist_id, \*)    | Remove every occurrence of `video_id` from `playlist_id`.                                                               |
| [`update_video_status`](_autosummary/yb.youtube.html.md#yb.youtube.update_video_status)(video_id, \*[, ...])           | Change a video's privacy (or synthetic-media flag), keeping the rest.                                                   |
| [`supersede`](_autosummary/yb.youtube.html.md#yb.youtube.supersede)(old_id, new_id, \*[, old_privacy, ...])  | Retire an upload that `new_id` replaces (yb#19).                                                                        |
| [`get_video`](_autosummary/yb.youtube.html.md#yb.youtube.get_video)(video_id, \*[, part, service])           | Fetch a video resource (raises `KeyError` if not found/visible).                                                        |
| [`upload_video`](_autosummary/yb.youtube.html.md#yb.youtube.upload_video)(video_path, body, \*[, service, ...]) | Resumably upload a video with the given `videos.insert` `body`.                                                         |
| [`update_video`](_autosummary/yb.youtube.html.md#yb.youtube.update_video)(video_id, snippet, \*[, service])     | Update a video's snippet (`videos.update`).                                                                             |
| [`update_video_fields`](_autosummary/yb.youtube.html.md#yb.youtube.update_video_fields)(video_id, \*[, title, ...])    | Patch selected snippet fields, preserving the rest.                                                                     |
| [`set_thumbnail`](_autosummary/yb.youtube.html.md#yb.youtube.set_thumbnail)(video_id, image_path, \*[, service]) | Set a custom thumbnail (`thumbnails.set`).                                                                              |
| [`set_chapters`](_autosummary/yb.youtube.html.md#yb.youtube.set_chapters)(video_id, chapters, \*[, ...])        | Insert/replace a chapters block in the video's description.                                                             |
| [`video_metadata`](_autosummary/yb.youtube.html.md#yb.youtube.video_metadata)(video_id, \*[, group, fields, ...]) | Fetch a video's live metadata & engagement numbers.                                                                     |
| [`flatten_video`](_autosummary/yb.youtube.html.md#yb.youtube.flatten_video)(resource)                            | Flatten a raw `videos.list` item into a friendly, typed, ordered dict.                                                  |
| [`select_fields`](_autosummary/yb.youtube.html.md#yb.youtube.select_fields)(flat, \*[, group, fields])           | Return an ordered subset of `flat` per `fields`/`group`.                                                                |
| [`resolve_fields`](_autosummary/yb.youtube.html.md#yb.youtube.resolve_fields)(\*[, group, fields, available])     | Resolve the ordered field list to show.                                                                                 |
| [`render_table`](_autosummary/yb.youtube.html.md#yb.youtube.render_table)(data, \*[, fields])                   | Render metadata as an ASCII table.                                                                                      |
| [`list_captions`](_autosummary/yb.youtube.html.md#yb.youtube.list_captions)(video_id, \*[, service])             | List caption tracks on a video (each item's snippet has language/name/trackKind/status).                                |
| [`insert_caption`](_autosummary/yb.youtube.html.md#yb.youtube.insert_caption)(video_id, path, \*, language)       | Insert a new caption track (`captions.insert`).                                                                         |
| [`update_caption`](_autosummary/yb.youtube.html.md#yb.youtube.update_caption)(caption_id, path, \*[, ...])        | Replace the content of an existing caption track (`captions.update`).                                                   |
| [`upsert_caption`](_autosummary/yb.youtube.html.md#yb.youtube.upsert_caption)(video_id, track, \*[, ...])         | Insert a caption track, or update the existing same-language one.                                                       |
| [`download_caption`](_autosummary/yb.youtube.html.md#yb.youtube.download_caption)(caption_id, \*[, tfmt, service])  | Download a caption track's content (`captions.download`), default SRT.                                                  |
| [`delete_caption`](_autosummary/yb.youtube.html.md#yb.youtube.delete_caption)(caption_id, \*[, service])          | Delete a caption track (`captions.delete`).                                                                             |
| [`publish_content`](_autosummary/yb.youtube.html.md#yb.youtube.publish_content)(content, \*[, ...])                | Upload a prepared `PublicationContent` to YouTube.                                                                      |
| [`prepare_and_publish`](_autosummary/yb.youtube.html.md#yb.youtube.prepare_and_publish)(media, \*[, language, ...])    | One call: prepare publication content from `media` and upload it.                                                       |
| [`publish_video`](_autosummary/yb.youtube.html.md#yb.youtube.publish_video)(video_path, metadata, \*[, ...])     | Lower-level upload: explicit [`VideoMetadata`](_autosummary/yb.youtube.html.md#yb.youtube.VideoMetadata) + caption tracks.           |

### Classes

| [`VideoMetadata`](_autosummary/yb.youtube.html.md#yb.youtube.VideoMetadata)(title, description[, tags, ...])   | YouTube-specific video metadata.      |
|---------------------------------------------------------------------------------------------------|---------------------------------------|
| [`CaptionTrack`](_autosummary/yb.youtube.html.md#yb.youtube.CaptionTrack)(path, language[, name, is_draft])   | A caption track to attach to a video. |

### Exceptions

| [`ConsentRequired`](_autosummary/yb.youtube.html.md#yb.youtube.ConsentRequired)                         | Consent is needed and this process cannot obtain it.                                                               |
|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
| [`ConsentPending`](_autosummary/yb.youtube.html.md#yb.youtube.ConsentPending)(url, \*, redirect_uri)   | Consent was started, and now waits for the user to paste the redirect back.                                        |
| [`VisibilityDropWarning`](_autosummary/yb.youtube.html.md#yb.youtube.VisibilityDropWarning)                   | [`supersede()`](_autosummary/yb.youtube.html.md#yb.youtube.supersede) leaves the re-cut less visible than the original was. |
| [`PartialPublish`](_autosummary/yb.youtube.html.md#yb.youtube.PartialPublish)(result, \*, step, error) | The video was uploaded, then a later step failed (yb#21).                                                          |

### *class* yb.youtube.CaptionTrack(path, language, name='', is_draft=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A caption track to attach to a video.

#### path

Path to an `.srt` (or `.vtt`) file.

#### language

BCP-47 language code, e.g. `"en"` or `"fr"`.

#### name

Track name shown in the YouTube UI (e.g. `"English"`).

#### is_draft

When `True`, uploaded but not published to viewers.

### *exception* yb.youtube.ConsentPending(url, , redirect_uri)

Bases: [`ConsentRequired`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.ConsentRequired)

Consent was started, and now waits for the user to paste the redirect back.

Not a failure: it is the *normal* result of the first call in paste mode.
`url` is the address to open; the message says what to do with the result.
Subclasses [`ConsentRequired`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.ConsentRequired) so a caller that only
handles “no credentials” still stops, while one that knows about paste mode
can catch this and relay `url`.

### *exception* yb.youtube.ConsentRequired

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Consent is needed and this process cannot obtain it.

Raised instead of starting a consent flow that nobody can complete. The
alternative is worse than an error: `run_local_server` prints a URL to a
stdout nobody is reading and waits on `localhost` for a redirect that will
never arrive, so an automated caller *blocks* rather than failing. That has
happened — an upload stopped dead mid-script and read as a slow network for
hours, when the underlying cause was one line of `invalid_grant`.

### *exception* yb.youtube.PartialPublish(result, , step, error)

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The video was uploaded, then a later step failed (yb#21).

The upload is the expensive step (1,600 quota units); captions, thumbnail
and playlist can each fail afterwards on quota, auth or a transient error.
Raising the bare error lost the new video’s id, and a re-run uploaded a
second copy. This carries what was done instead:

- `result`: the dict the publish call would have returned, as far as it
  got (`video_id`, `url`, `captions` attached so far, `thumbnail`,
  `playlist`). Persist it.
- `step`: the step that failed, e.g. `"caption ja"`.
- `__cause__`: the original error.

Finish later without re-uploading: pass `resume=err.result` to the same
call ([`publish_video()`](_autosummary/yb.youtube.html.md#yb.youtube.publish_video) or [`publish_content()`](_autosummary/yb.youtube.html.md#yb.youtube.publish_content); for
[`prepare_and_publish()`](_autosummary/yb.youtube.html.md#yb.youtube.prepare_and_publish), call [`publish_content()`](_autosummary/yb.youtube.html.md#yb.youtube.publish_content) with the content
it prepared, rather than preparing it again). Steps already done are
skipped. A resume trusts the saved result: it does not upload, so new
metadata, video path or privacy in the resuming call are ignored, and the
remaining steps target the saved `video_id`.

`result` is plain JSON-able data, and the exception pickles (so a worker
process can hand it back); `__cause__` does not survive pickling.

### *class* yb.youtube.VideoMetadata(title, description, tags=<factory>, category_id='28', default_language=None, default_audio_language=None, contains_synthetic_media=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

YouTube-specific video metadata.

#### title

Title (<= 100 chars).

#### description

Description (<= 5000 chars), chapters already embedded.

#### tags

Keyword tags (combined length kept under 500 chars).

#### category_id

YouTube category id (default Science & Technology).

#### default_language

BCP-47 language of the metadata text.

#### default_audio_language

BCP-47 language of the audio.

#### contains_synthetic_media

Declare that the video contains realistic
altered or synthetic content. YouTube’s disclosure rule turns on
*realism*, not on whether AI was involved: it applies when a viewer
could mistake the content for a real person, place, or event —
not to stylised artwork or a visualizer. Left unset when `None`.

#### *classmethod* from_content(content, , category_id='28', with_chapters=True)

Build YouTube metadata from a platform-neutral content bundle.

Embeds the chapters block into the description (when present and
`with_chapters`), so YouTube renders interactive chapters.

* **Return type:**
  [`VideoMetadata`](_autosummary/yb.youtube.metadata.html.md#yb.youtube.metadata.VideoMetadata)

#### insert_body(, privacy_status='unlisted')

Build the `videos.insert` body (snippet + status).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### update_snippet()

Build the snippet for `videos.update` (categoryId is required).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### *exception* yb.youtube.VisibilityDropWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

[`supersede()`](_autosummary/yb.youtube.html.md#yb.youtube.supersede) leaves the re-cut less visible than the original was.

Emitted before anything is changed, so escalating it refuses the call:

```default
warnings.simplefilter("error", VisibilityDropWarning)
```

### yb.youtube.add_stats(videos, , api_key=None, backend='auto')

Fill in engagement numbers on search results, in place-ish (returns them).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### yb.youtube.add_to_playlist(video_id, playlist_id, , position=None, service=None, \*\*cred_kwargs)

Add `video_id` to `playlist_id` (`playlistItems.insert`).

Appended at the end, or inserted at the 0-based `position` when given
(YouTube refuses a position on a playlist not sorted manually).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.add_video_to_playlist(video_id, title, , create=True, privacy_status='private', skip_if_present=True, service=None, \*\*cred_kwargs)

Find-or-create the playlist named `title` and append `video_id`.

Idempotent: with `skip_if_present` (default), a video already in the
playlist is left alone instead of duplicated.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  `{"playlist_id", "playlist_title", "added", "created"}` where `added`
  is `False` if the video was already present and `created` is `True`
  if the playlist had to be made.
* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – the playlist is absent and `create=False`.

### yb.youtube.api_available(, api_key=None)

Whether the Data API can actually be called with the key we have.

A key that exists is not a key that works: the API has to be enabled on its
Cloud project too, and that failure is a 403 at call time rather than
anything visible up front.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### yb.youtube.create_playlist(title, , description='', privacy_status='private', service=None, \*\*cred_kwargs)

Create a playlist and return its id.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.youtube.default_client_secrets_file()

Default OAuth client-secrets location: `<config dir>/client_secret.json`.

Used when neither `client_secrets_file=` nor the env vars are set, so all
of `yb`’s state can live in one directory next to the token.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### yb.youtube.default_token_file()

Cached OAuth token location (`$XDG_CONFIG_HOME` or `~/.config`).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### yb.youtube.delete_caption(caption_id, , service=None, \*\*cred_kwargs)

Delete a caption track (`captions.delete`).

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.youtube.download_caption(caption_id, , tfmt='srt', service=None, \*\*cred_kwargs)

Download a caption track’s content (`captions.download`), default SRT.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### yb.youtube.ensure_playlist(title, , create=True, description='', privacy_status='private', service=None, \*\*cred_kwargs)

Return the id of the playlist titled `title`, creating it if missing.

Returns `None` only when the playlist is absent and `create=False`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.youtube.find_playlist(title, , service=None, \*\*cred_kwargs)

Return the id of the caller’s playlist titled `title` (or `None`).

Matches the first playlist with an exact title; YouTube allows duplicate
titles, so prefer unique playlist names.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.youtube.finish_paste_consent(authorization_response, , client_secrets_file=None, pending_file=None)

Complete a consent begun by [`start_paste_consent()`](_autosummary/yb.youtube.html.md#yb.youtube.start_paste_consent); return credentials.

`authorization_response` is the redirected URL, its query string, or the
bare `code`. The pending file is removed on success and left in place on
failure, so a mistyped paste can simply be retried.

### yb.youtube.first_position(video_id, playlist_id, , service=None, \*\*cred_kwargs)

The 0-based position of `video_id`’s first occurrence (`None` if absent).

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.youtube.flatten_video(resource)

Flatten a raw `videos.list` item into a friendly, typed, ordered dict.

Pure: pass the dict returned by the API. Missing pieces (from a partial
`part=` request, disabled stats, or an unauthorized dislike count) become
`None` rather than raising, so callers never have to guard the shape.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.get_credentials(, client_secrets_file=None, token_file=None, scopes=['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl'], open_browser=True, port=0, timeout_seconds=300.0, interactive=True, consent='local', authorization_response=None)

Return OAuth user credentials, running the consent flow if needed.

First use runs the installed-app consent flow and caches the token to
`token_file` so later calls are non-interactive. Expired tokens are
refreshed automatically.

Pass `interactive=False` when nobody can answer a consent prompt — a cron
job, a queue worker, an agent. Consent is then never started; a token that
cannot be refreshed raises [`ConsentRequired`](_autosummary/yb.youtube.html.md#yb.youtube.ConsentRequired) naming the cause and the
fix. This is not an edge case: an OAuth client left in “Testing” rotates
refresh tokens out after about seven days, so anything scheduled hits it
weekly.

Even with the default `interactive=True` the call can no longer hang:
consent is bounded by `timeout_seconds`
(`DEFAULT_CONSENT_TIMEOUT_S`), and a timeout is reported as the same
[`ConsentRequired`](_autosummary/yb.youtube.html.md#yb.youtube.ConsentRequired), because “nobody answered” and “nobody could
answer” want the same thing done about them.

Consent always goes through a temporary local web server on `port` (`0`
picks a free one), because Google retired the copy-paste “out-of-band” flow
in 2022. `open_browser=False` only stops the browser from being launched:
the authorization URL is printed instead, and the redirect must still reach
that local server.

Headless recipe: pass `interactive=True` (there is no terminal, so the
default would refuse), `open_browser=False` and a fixed `port=`, then
forward that port from the machine holding the browser
(`ssh -L <port>:localhost:<port> <host>`). Nothing needs registering in
the Cloud console — the *Desktop app* client this module requires accepts
any `localhost` port, which is also why the `port=0` default works.

The call blocks until the redirect arrives, bounded by `timeout_seconds`
(default `DEFAULT_CONSENT_TIMEOUT_S`; pass `None` for the library’s
“wait indefinitely”, which is what this used to do).

\*\*No shared `localhost`? Use\*\* `consent="paste"`. Consent then needs no
running server and no waiting: the first call returns by raising
[`ConsentPending`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.ConsentPending) carrying the URL to open
(anywhere — a phone will do); the browser’s redirect to `localhost` fails
to load, and the second call passes that address-bar URL (or just its
`code`) as `authorization_response=` to finish and cache the token. The
two calls may be separate turns or separate processes; `yb auth` is the
command-line form. `port` only names the (unreachable) redirect.

These keywords ride `**cred_kwargs` through [`get_service()`](_autosummary/yb.youtube.html.md#yb.youtube.get_service) and the
publishing helpers. The few entry points that take none (notably
`yb.music.publish.publish_folder()`) still work headlessly: call this
once to mint the token, after which nothing prompts again.

### yb.youtube.get_service(, credentials=None, \*\*cred_kwargs)

Build a YouTube Data API v3 service object.

`cred_kwargs` are forwarded to [`get_credentials()`](_autosummary/yb.youtube.html.md#yb.youtube.get_credentials) (`token_file=`,
`open_browser=`, `port=`, …) unless `credentials` is given.

### yb.youtube.get_video(video_id, , part='snippet,status', service=None, \*\*cred_kwargs)

Fetch a video resource (raises `KeyError` if not found/visible).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.insert_caption(video_id, path, , language, name='', is_draft=False, service=None, \*\*cred_kwargs)

Insert a new caption track (`captions.insert`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.is_video_in_playlist(video_id, playlist_id, , service=None, \*\*cred_kwargs)

Whether `video_id` is already an item of `playlist_id` (paginated).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### yb.youtube.list_captions(video_id, , service=None, \*\*cred_kwargs)

List caption tracks on a video (each item’s snippet has language/name/trackKind/status).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### yb.youtube.list_my_playlists(, service=None, \*\*cred_kwargs)

Return all playlists owned by the authenticated channel (paginated).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### yb.youtube.playlist_item_ids(video_id, playlist_id, , service=None, \*\*cred_kwargs)

The `playlistItems` ids holding `video_id` in `playlist_id`.

A list, because a video can sit in one playlist more than once.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### yb.youtube.playlists_containing(video_id, , service=None, \*\*cred_kwargs)

Ids of the authenticated channel’s playlists that hold `video_id`.

Reads every item page of every playlist you own (1 quota unit per page),
plus the `playlists.list` pages: on a large channel, budget for it.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### yb.youtube.prepare_and_publish(media, \*, language='English', language_code=None, audio_language_code=None, brand=None, extra_context=None, with_chapters=True, with_thumbnail=True, privacy_status=None, playlist=<object object>, category_id='28', config=None, client_secrets_file=None, token_file=None, progress=True, \*\*cred_kwargs)

One call: prepare publication content from `media` and upload it.

Transcribes (persisting the SRT next to the media) if needed, writes
LLM metadata, detects chapters, renders a thumbnail, then uploads with
captions + thumbnail + chapters and adds the video to the configured
playlist. `privacy_status` and `playlist` default to your `yb` config
(see [`publish_content()`](_autosummary/yb.youtube.html.md#yb.youtube.publish_content)).

`cred_kwargs` reach [`get_credentials()`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_credentials) (e.g.
`open_browser=False, port=8080` for headless consent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.publish_content(content, \*, privacy_status=None, playlist=<object object>, category_id='28', with_chapters=True, attach_caption=True, set_thumb=True, config=None, client_secrets_file=None, token_file=None, progress=True, service=None, resume=None, \*\*cred_kwargs)

Upload a prepared `PublicationContent` to YouTube.

Maps the content to a YouTube snippet (chapters embedded in the
description when present), uploads, attaches the SRT caption track
(language = `content.audio_language` or `content.language`) and the
thumbnail when available, and adds the video to the configured playlist.

`privacy_status` and `playlist` default to your `yb` config (see
[`yb.config`](_autosummary/yb.config.html.md#module-yb.config)): unset means “use the config value”, so privacy falls back
to `unlisted` and the video joins your configured playlist. Pass
`playlist=None` to skip the playlist for one call, or `playlist="Name"`
to override the target.

`cred_kwargs` reach [`get_credentials()`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_credentials) (e.g.
`open_browser=False, port=8080` for headless consent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  `{"video_id", "url", "studio_url", "privacy_status", "captions",
  "thumbnail", "playlist"}`. `privacy_status` reflects what YouTube
  actually set (an unaudited project may force `"private"`); `playlist`
  is the [`add_video_to_playlist()`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.add_video_to_playlist) result or
  `None`.
* **Raises:**
  [**PartialPublish**](_autosummary/yb.youtube.html.md#yb.youtube.PartialPublish) – the upload succeeded and a later step failed;
      `err.result` holds the video id and what was done. Pass
      `resume=err.result` to finish without uploading again (yb#21).

### yb.youtube.publish_video(video_path, metadata, \*, privacy_status=None, playlist=<object object>, captions=None, thumbnail=None, config=None, client_secrets_file=None, token_file=None, progress=True, service=None, resume=None, \*\*cred_kwargs)

Lower-level upload: explicit [`VideoMetadata`](_autosummary/yb.youtube.html.md#yb.youtube.VideoMetadata) + caption tracks.

`privacy_status` and `playlist` default to your `yb` config (see
[`publish_content()`](_autosummary/yb.youtube.html.md#yb.youtube.publish_content)).

If a step after the upload fails, [`PartialPublish`](_autosummary/yb.youtube.html.md#yb.youtube.PartialPublish) is raised with
the new video’s id and what was done so far (`err.result`). Pass
`resume=err.result` to finish without uploading again (yb#21).

`cred_kwargs` reach [`get_credentials()`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_credentials) (e.g.
`open_browser=False, port=8080` for headless consent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.rank_by_engagement(videos, , min_views=0, like_weight=1.0, view_weight=1.0)

Order videos by a like-rate-and-reach score.

Raw view count alone rewards age and luck, and like count alone rewards the
same. The score multiplies *reach* (log views, so an order of magnitude
counts for a fixed amount) by *approval* (likes per thousand views), which
ranks a well-liked mid-sized video above a big indifferent one. Videos
missing numbers sort last rather than being dropped, since absent is not the
same as bad.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### yb.youtube.remove_from_playlist(video_id, playlist_id, , service=None, \*\*cred_kwargs)

Remove every occurrence of `video_id` from `playlist_id`.

Returns how many items were removed (0 when it was not there).

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)

### yb.youtube.render_table(data, , fields=None)

Render metadata as an ASCII table.

A single flat dict renders as a two-column `field | value` table; a list
of flat dicts renders one row per video with `fields` as columns.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.youtube.resolve_fields(, group=None, fields=None, available=None)

Resolve the ordered field list to show.

Precedence: explicit `fields` > named `group` > every `available`
field. An unknown `group` raises `KeyError` naming the valid options.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### yb.youtube.search_videos(query, , max_results=10, backend='auto', with_stats=False, api_key=None, language=None, region=None, \*\*params)

Search YouTube and return a list of video records.

`language`/`region` bias results (`"en"`, `"GB"`) rather than filter
them — YouTube offers no hard language filter. Check what comes back if the
language matters; a title can be auto-translated by the interface, which
makes an English video look like a local one.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### yb.youtube.select_fields(flat, , group=None, fields=None)

Return an ordered subset of `flat` per `fields`/`group`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.set_chapters(video_id, chapters, , header='Chapters:', service=None, \*\*cred_kwargs)

Insert/replace a chapters block in the video’s description.

Strips any prior block under `header` and appends the new one. YouTube
renders interactive chapters when the first timestamp is `0:00`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.set_thumbnail(video_id, image_path, , service=None, \*\*cred_kwargs)

Set a custom thumbnail (`thumbnails.set`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.start_paste_consent(, client_secrets_file=None, scopes=['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl'], port=8080, pending_file=None, reuse_pending=True)

Begin paste-back consent and return the URL to open.

A consent already pending for the same client, `scopes` and `port` is reused
(`reuse_pending=True`): re-printing the URL must not invalidate the one the
user already has open on their phone.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.youtube.supersede(old_id, new_id, , old_privacy='private', new_privacy=None, move_playlists=True, service=None, \*\*cred_kwargs)

Retire an upload that `new_id` replaces (yb#19).

YouTube cannot replace a video’s file, so a re-cut is a new upload. This
puts the new one where the old one was and takes the old one out of view:

1. refuses, before changing anything, unless `new_id` has finished
   processing (`uploadStatus == "processed"`): retiring the old video for
   a failed or rejected upload would leave the public neither;
2. sets `new_id` to `new_privacy` when given (e.g. `"public"`);
3. for every playlist of yours that holds `old_id` (when
   `move_playlists`): inserts `new_id` at the old one’s position (or
   appends, if the playlist is not sorted manually), THEN removes
   `old_id`, so a failure part-way never leaves a playlist with neither;
4. sets `old_id` to `old_privacy` (default `"private"`), clearing any
   scheduled publish. It is never deleted.

The new video keeps its own privacy unless `new_privacy` is given, and an
upload is `unlisted` by default: so the re-cut of a public video is NOT
made public for you. When the result is less visible than the old video
was (a public original, an unlisted re-cut), a [`VisibilityDropWarning`](_autosummary/yb.youtube.html.md#yb.youtube.VisibilityDropWarning)
is emitted before anything changes; pass `new_privacy="public"` to publish
the re-cut, or turn the warning into an error to refuse such calls.

Captions and thumbnails are not copied: upload them with the new video
([`yb.youtube.publish_video()`](_autosummary/yb.youtube.html.md#yb.youtube.publish_video)). Safe to re-run: every step checks
before it acts. Quota: see [`yb.youtube.playlists_containing()`](_autosummary/yb.youtube.html.md#yb.youtube.playlists_containing); each
write (insert, delete, update) is 50 units.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  `{"old_id", "new_id", "playlists", "old_privacy", "new_privacy"}`
  where `playlists` lists `{"playlist_id", "added_new", "position",
  "removed_old"}`.

### yb.youtube.thumbnail_url(video_id, , quality='mqdefault')

Thumbnail URL for a video. Always available, no API call, no key.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.youtube.update_caption(caption_id, path, , is_draft=None, service=None, \*\*cred_kwargs)

Replace the content of an existing caption track (`captions.update`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.update_video(video_id, snippet, , service=None, \*\*cred_kwargs)

Update a video’s snippet (`videos.update`). `categoryId` is required.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.update_video_fields(video_id, , title=None, description=None, tags=None, category_id=None, default_language=None, default_audio_language=None, service=None, \*\*cred_kwargs)

Patch selected snippet fields, preserving the rest.

Fetches the current snippet, overlays the provided fields, and updates.
`categoryId` must be present (kept from the existing snippet if not given).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.update_video_status(video_id, , privacy_status=None, contains_synthetic_media=None, keep_schedule=False, service=None, \*\*cred_kwargs)

Change a video’s privacy (or synthetic-media flag), keeping the rest.

Read-modify-write of the `status` part (yb#19): `videos.update` replaces
the part wholesale, so the current writable fields are carried over and
only the ones given change. Making a video private never deletes it.

Setting `privacy_status` clears a scheduled publish (`publishAt`):
otherwise a video made private while scheduled would still go public at
that time, and a past `publishAt` read back is refused by the API. Pass
`keep_schedule=True` to keep it (only meaningful with `"private"`).

* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – `privacy_status` is not one of `PRIVACY_STATUSES`.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.upload_video(video_path, body, , service=None, chunksize=8388608, progress=True, \*\*cred_kwargs)

Resumably upload a video with the given `videos.insert` `body`.

Returns the created video resource (includes `id`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.upsert_caption(video_id, track, , language=None, name='', is_draft=False, replace=True, service=None, \*\*cred_kwargs)

Insert a caption track, or update the existing same-language one.

* **Parameters:**
  * **video_id** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Target video.
  * **track** ([`CaptionTrack`](_autosummary/yb.youtube.captions.html.md#yb.youtube.captions.CaptionTrack) | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)) – A [`CaptionTrack`](_autosummary/yb.youtube.html.md#yb.youtube.CaptionTrack), or a path (then `language` required).
  * **replace** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When an uploaded track in the same language exists, update it
    (`True`, default) rather than inserting a duplicate.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  The inserted/updated caption resource.

### yb.youtube.video_metadata(video_id, , group=None, fields=None, part='snippet,statistics,contentDetails,status', as_table=False, service=None, \*\*cred_kwargs)

Fetch a video’s live metadata & engagement numbers.

* **Parameters:**
  * **video_id** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – A single video id or an iterable of ids (batched, ≤50/call).
  * **group** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Name of a preset field set from `FIELD_GROUPS`
    (e.g. `"engagement"`). Ignored if `fields` is given.
  * **fields** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – Explicit ordered field names to keep (overrides `group`).
    With neither `group` nor `fields` you get every field.
  * **part** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `videos.list` parts to request (default covers all fields).
  * **as_table** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When `True`, return a ready-to-print ASCII table string
    instead of the dict/list.
  * **service** – An authenticated YouTube service; else built from
    `cred_kwargs` (e.g. `token_file=...`).
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)] | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  A flat `dict` for one id / `list[dict]` for many — or an ASCII
  table `str` when `as_table=True`.

### Modules

| [`api`](_autosummary/yb.youtube.api.html.md#module-yb.youtube.api)                     | YouTube video operations: get, upload, update metadata, thumbnail, chapters.   |
|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`auth`](_autosummary/yb.youtube.auth.html.md#module-yb.youtube.auth)                   | OAuth 2.0 plumbing for the YouTube Data API v3.                                |
| [`captions`](_autosummary/yb.youtube.captions.html.md#module-yb.youtube.captions)           | Caption (subtitle) tracks on YouTube videos: list, insert, update, upsert.     |
| [`metadata`](_autosummary/yb.youtube.metadata.html.md#module-yb.youtube.metadata)           | YouTube video metadata ↔ API snippet mapping.                                  |
| [`paste_consent`](_autosummary/yb.youtube.paste_consent.html.md#module-yb.youtube.paste_consent) | Paste-back OAuth consent: consent from a machine nobody is sitting at.         |
| [`playlists`](_autosummary/yb.youtube.playlists.html.md#module-yb.youtube.playlists)         | YouTube playlist operations: find/create a playlist and add videos to it.      |
| [`publish`](_autosummary/yb.youtube.publish.html.md#module-yb.youtube.publish)             | High-level YouTube publishing: prepare → upload → captions → thumbnail.        |
| [`search`](_autosummary/yb.youtube.search.html.md#module-yb.youtube.search)               | Search YouTube for videos, with engagement numbers to rank them by.            |
| [`stats`](_autosummary/yb.youtube.stats.html.md#module-yb.youtube.stats)                 | Read live video metadata & engagement numbers from the YouTube Data API v3.    |


# _autosummary/yb.youtube.metadata.html.md

# yb.youtube.metadata

YouTube video metadata ↔ API snippet mapping.

Maps the platform-neutral [`yb.content.PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent) onto a
YouTube `videos.insert` body, applying YouTube’s field limits (title <= 100
chars, description <= 5000, tags combined <= 500) and embedding the chapters
block into the description (YouTube renders chapters from description
timestamps when the first is `0:00`).

### Module Attributes

| [`CATEGORY_SCIENCE_TECH`](_autosummary/yb.youtube.metadata.html.md#yb.youtube.metadata.CATEGORY_SCIENCE_TECH)   | A useful subset of YouTube video category ids.   |
|--------------------------------------------------------------------------|--------------------------------------------------|

### Classes

| [`VideoMetadata`](_autosummary/yb.youtube.metadata.html.md#yb.youtube.metadata.VideoMetadata)(title, description[, tags, ...])   | YouTube-specific video metadata.   |
|---------------------------------------------------------------------------------------------------|------------------------------------|

### yb.youtube.metadata.CATEGORY_SCIENCE_TECH *= '28'*

A useful subset of YouTube video category ids.

### *class* yb.youtube.metadata.VideoMetadata(title, description, tags=<factory>, category_id='28', default_language=None, default_audio_language=None, contains_synthetic_media=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

YouTube-specific video metadata.

#### title

Title (<= 100 chars).

#### description

Description (<= 5000 chars), chapters already embedded.

#### tags

Keyword tags (combined length kept under 500 chars).

#### category_id

YouTube category id (default Science & Technology).

#### default_language

BCP-47 language of the metadata text.

#### default_audio_language

BCP-47 language of the audio.

#### contains_synthetic_media

Declare that the video contains realistic
altered or synthetic content. YouTube’s disclosure rule turns on
*realism*, not on whether AI was involved: it applies when a viewer
could mistake the content for a real person, place, or event —
not to stylised artwork or a visualizer. Left unset when `None`.

#### *classmethod* from_content(content, , category_id='28', with_chapters=True)

Build YouTube metadata from a platform-neutral content bundle.

Embeds the chapters block into the description (when present and
`with_chapters`), so YouTube renders interactive chapters.

* **Return type:**
  [`VideoMetadata`](_autosummary/yb.youtube.metadata.html.md#yb.youtube.metadata.VideoMetadata)

#### insert_body(, privacy_status='unlisted')

Build the `videos.insert` body (snippet + status).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### update_snippet()

Build the snippet for `videos.update` (categoryId is required).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/yb.youtube.paste_consent.html.md

# yb.youtube.paste_consent

Paste-back OAuth consent: consent from a machine nobody is sitting at.

The normal flow (`InstalledAppFlow.run_local_server`) keeps a web server up on
`localhost` and waits for Google to redirect the browser to it. That needs the
browser and the process to share a `localhost` — which a session driven from a
phone does not have: the consent page opens on the phone, the redirect goes to
the *phone’s* `localhost`, and the process (no terminal, nothing to wait on)
gives up with [`ConsentRequired`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.ConsentRequired).

Paste-back splits consent into two steps that need no shared network and no
live process in between, so they can happen in different turns, even in
different Python processes:

1. [`start_paste_consent()`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.start_paste_consent) builds the authorization URL and saves the one
   thing the second step needs (the PKCE verifier and `state`) to a short-lived
   file next to the token. The user opens the URL anywhere and consents.
2. Google redirects to `http://localhost:<port>/?state=…&code=…`. That page
   will not load — expected. The user copies the address-bar URL (or just the
   `code`) back, and [`finish_paste_consent()`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.finish_paste_consent) completes the exchange.

Most callers never import this module: `get_credentials(consent="paste")` and
the `yb auth` command drive it. Nothing here writes the token — the caller
(`get_credentials`) does, so there is one place that does.

### Module Attributes

| [`DEFAULT_PASTE_PORT`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.DEFAULT_PASTE_PORT)   | The redirect target.                                                                                                    |
|-----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| [`PENDING_TTL_S`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.PENDING_TTL_S)        | How long a printed consent URL stays usable by [`finish_paste_consent()`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.finish_paste_consent). |

### Functions

| [`finish_paste_consent`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.finish_paste_consent)(authorization_response, \*)   | Complete a consent begun by [`start_paste_consent()`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.start_paste_consent); return credentials.   |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
| [`pending_consent_file`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.pending_consent_file)()                             | Where the in-progress consent is kept (next to the token, mode 0600).                                                     |
| [`start_paste_consent`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.start_paste_consent)(\*[, ...])                     | Begin paste-back consent and return the URL to open.                                                                      |

### Exceptions

| [`ConsentPending`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.ConsentPending)(url, \*, redirect_uri)   | Consent was started, and now waits for the user to paste the redirect back.   |
|------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|

### *exception* yb.youtube.paste_consent.ConsentPending(url, , redirect_uri)

Bases: [`ConsentRequired`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.ConsentRequired)

Consent was started, and now waits for the user to paste the redirect back.

Not a failure: it is the *normal* result of the first call in paste mode.
`url` is the address to open; the message says what to do with the result.
Subclasses [`ConsentRequired`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.ConsentRequired) so a caller that only
handles “no credentials” still stops, while one that knows about paste mode
can catch this and relay `url`.

### yb.youtube.paste_consent.DEFAULT_PASTE_PORT *= 8080*

The redirect target. Nothing listens there in paste mode — the page failing to
load is the expected outcome; only its address-bar URL matters.

### yb.youtube.paste_consent.PENDING_TTL_S *= 86400.0*

How long a printed consent URL stays usable by [`finish_paste_consent()`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.finish_paste_consent).
Generous, because the gap is a human’s: a phone-driven session may take hours
to come back with the redirect. Google’s own limit is on the one-time *code*
it issues after consent, not on the URL, so this only bounds our bookkeeping.

### yb.youtube.paste_consent.finish_paste_consent(authorization_response, , client_secrets_file=None, pending_file=None)

Complete a consent begun by [`start_paste_consent()`](_autosummary/yb.youtube.paste_consent.html.md#yb.youtube.paste_consent.start_paste_consent); return credentials.

`authorization_response` is the redirected URL, its query string, or the
bare `code`. The pending file is removed on success and left in place on
failure, so a mistyped paste can simply be retried.

### yb.youtube.paste_consent.pending_consent_file()

Where the in-progress consent is kept (next to the token, mode 0600).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### yb.youtube.paste_consent.start_paste_consent(, client_secrets_file=None, scopes=['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl'], port=8080, pending_file=None, reuse_pending=True)

Begin paste-back consent and return the URL to open.

A consent already pending for the same client, `scopes` and `port` is reused
(`reuse_pending=True`): re-printing the URL must not invalidate the one the
user already has open on their phone.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/yb.youtube.playlists.html.md

# yb.youtube.playlists

YouTube playlist operations: find/create a playlist and add videos to it.

Wraps the Data API `playlists` and `playlistItems` resources (writable with
the `youtube.force-ssl` scope `yb` already requests — no extra consent). The
high-level [`add_video_to_playlist()`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.add_video_to_playlist) works from a stable human *title* like
`"TW Uploads"`: it finds the playlist (optionally creating it), skips the add
if the video is already in it, and appends otherwise — so re-publishing or
re-running is idempotent.

### Functions

| [`add_to_playlist`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.add_to_playlist)(video_id, playlist_id, \*[, ...])   | Add `video_id` to `playlist_id` (`playlistItems.insert`).                 |
|------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| [`add_video_to_playlist`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.add_video_to_playlist)(video_id, title, \*[, ...])   | Find-or-create the playlist named `title` and append `video_id`.          |
| [`create_playlist`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.create_playlist)(title, \*[, description, ...])      | Create a playlist and return its id.                                      |
| [`ensure_playlist`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.ensure_playlist)(title, \*[, create, ...])           | Return the id of the playlist titled `title`, creating it if missing.     |
| [`find_playlist`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.find_playlist)(title, \*[, service])                 | Return the id of the caller's playlist titled `title` (or `None`).        |
| [`first_position`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.first_position)(video_id, playlist_id, \*[, ...])    | The 0-based position of `video_id`'s first occurrence (`None` if absent). |
| [`is_video_in_playlist`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.is_video_in_playlist)(video_id, playlist_id, \*)     | Whether `video_id` is already an item of `playlist_id` (paginated).       |
| [`list_my_playlists`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.list_my_playlists)(\*[, service])                    | Return all playlists owned by the authenticated channel (paginated).      |
| [`playlist_item_ids`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.playlist_item_ids)(video_id, playlist_id, \*)        | The `playlistItems` ids holding `video_id` in `playlist_id`.              |
| [`playlists_containing`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.playlists_containing)(video_id, \*[, service])       | Ids of the authenticated channel's playlists that hold `video_id`.        |
| [`remove_from_playlist`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.remove_from_playlist)(video_id, playlist_id, \*)     | Remove every occurrence of `video_id` from `playlist_id`.                 |

### yb.youtube.playlists.add_to_playlist(video_id, playlist_id, , position=None, service=None, \*\*cred_kwargs)

Add `video_id` to `playlist_id` (`playlistItems.insert`).

Appended at the end, or inserted at the 0-based `position` when given
(YouTube refuses a position on a playlist not sorted manually).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.playlists.add_video_to_playlist(video_id, title, , create=True, privacy_status='private', skip_if_present=True, service=None, \*\*cred_kwargs)

Find-or-create the playlist named `title` and append `video_id`.

Idempotent: with `skip_if_present` (default), a video already in the
playlist is left alone instead of duplicated.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  `{"playlist_id", "playlist_title", "added", "created"}` where `added`
  is `False` if the video was already present and `created` is `True`
  if the playlist had to be made.
* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – the playlist is absent and `create=False`.

### yb.youtube.playlists.create_playlist(title, , description='', privacy_status='private', service=None, \*\*cred_kwargs)

Create a playlist and return its id.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.youtube.playlists.ensure_playlist(title, , create=True, description='', privacy_status='private', service=None, \*\*cred_kwargs)

Return the id of the playlist titled `title`, creating it if missing.

Returns `None` only when the playlist is absent and `create=False`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.youtube.playlists.find_playlist(title, , service=None, \*\*cred_kwargs)

Return the id of the caller’s playlist titled `title` (or `None`).

Matches the first playlist with an exact title; YouTube allows duplicate
titles, so prefer unique playlist names.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.youtube.playlists.first_position(video_id, playlist_id, , service=None, \*\*cred_kwargs)

The 0-based position of `video_id`’s first occurrence (`None` if absent).

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### yb.youtube.playlists.is_video_in_playlist(video_id, playlist_id, , service=None, \*\*cred_kwargs)

Whether `video_id` is already an item of `playlist_id` (paginated).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### yb.youtube.playlists.list_my_playlists(, service=None, \*\*cred_kwargs)

Return all playlists owned by the authenticated channel (paginated).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### yb.youtube.playlists.playlist_item_ids(video_id, playlist_id, , service=None, \*\*cred_kwargs)

The `playlistItems` ids holding `video_id` in `playlist_id`.

A list, because a video can sit in one playlist more than once.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### yb.youtube.playlists.playlists_containing(video_id, , service=None, \*\*cred_kwargs)

Ids of the authenticated channel’s playlists that hold `video_id`.

Reads every item page of every playlist you own (1 quota unit per page),
plus the `playlists.list` pages: on a large channel, budget for it.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### yb.youtube.playlists.remove_from_playlist(video_id, playlist_id, , service=None, \*\*cred_kwargs)

Remove every occurrence of `video_id` from `playlist_id`.

Returns how many items were removed (0 when it was not there).

* **Return type:**
  [`int`](https://docs.python.org/3/builtins/functions.html#int)


# _autosummary/yb.youtube.publish.html.md

# yb.youtube.publish

High-level YouTube publishing: prepare → upload → captions → thumbnail.

Two entry points:

> - [`publish_content()`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.publish_content) — given a prepared
>   [`yb.content.PublicationContent`](_autosummary/yb.content.html.md#yb.content.PublicationContent), upload it with captions,
>   > thumbnail, and chapters (in the description).
> - [`prepare_and_publish()`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.prepare_and_publish) — the one-call path: transcribe/derive
>   metadata/detect chapters (via [`yb.content.prepare_content()`](_autosummary/yb.content.html.md#yb.content.prepare_content)) and
>   upload, all from a media path.

### Functions

| [`prepare_and_publish`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.prepare_and_publish)(media, \*[, language, ...])   | One call: prepare publication content from `media` and upload it.   |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------|
| [`publish_content`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.publish_content)(content, \*[, ...])               | Upload a prepared `PublicationContent` to YouTube.                  |
| [`publish_video`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.publish_video)(video_path, metadata, \*[, ...])    | Lower-level upload: explicit `VideoMetadata` + caption tracks.      |

### Exceptions

| [`PartialPublish`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.PartialPublish)(result, \*, step, error)   | The video was uploaded, then a later step failed (yb#21).   |
|--------------------------------------------------------------------------------------------|-------------------------------------------------------------|

### *exception* yb.youtube.publish.PartialPublish(result, , step, error)

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

The video was uploaded, then a later step failed (yb#21).

The upload is the expensive step (1,600 quota units); captions, thumbnail
and playlist can each fail afterwards on quota, auth or a transient error.
Raising the bare error lost the new video’s id, and a re-run uploaded a
second copy. This carries what was done instead:

- `result`: the dict the publish call would have returned, as far as it
  got (`video_id`, `url`, `captions` attached so far, `thumbnail`,
  `playlist`). Persist it.
- `step`: the step that failed, e.g. `"caption ja"`.
- `__cause__`: the original error.

Finish later without re-uploading: pass `resume=err.result` to the same
call ([`publish_video()`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.publish_video) or [`publish_content()`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.publish_content); for
[`prepare_and_publish()`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.prepare_and_publish), call [`publish_content()`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.publish_content) with the content
it prepared, rather than preparing it again). Steps already done are
skipped. A resume trusts the saved result: it does not upload, so new
metadata, video path or privacy in the resuming call are ignored, and the
remaining steps target the saved `video_id`.

`result` is plain JSON-able data, and the exception pickles (so a worker
process can hand it back); `__cause__` does not survive pickling.

### yb.youtube.publish.prepare_and_publish(media, \*, language='English', language_code=None, audio_language_code=None, brand=None, extra_context=None, with_chapters=True, with_thumbnail=True, privacy_status=None, playlist=<object object>, category_id='28', config=None, client_secrets_file=None, token_file=None, progress=True, \*\*cred_kwargs)

One call: prepare publication content from `media` and upload it.

Transcribes (persisting the SRT next to the media) if needed, writes
LLM metadata, detects chapters, renders a thumbnail, then uploads with
captions + thumbnail + chapters and adds the video to the configured
playlist. `privacy_status` and `playlist` default to your `yb` config
(see [`publish_content()`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.publish_content)).

`cred_kwargs` reach [`get_credentials()`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_credentials) (e.g.
`open_browser=False, port=8080` for headless consent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.publish.publish_content(content, \*, privacy_status=None, playlist=<object object>, category_id='28', with_chapters=True, attach_caption=True, set_thumb=True, config=None, client_secrets_file=None, token_file=None, progress=True, service=None, resume=None, \*\*cred_kwargs)

Upload a prepared `PublicationContent` to YouTube.

Maps the content to a YouTube snippet (chapters embedded in the
description when present), uploads, attaches the SRT caption track
(language = `content.audio_language` or `content.language`) and the
thumbnail when available, and adds the video to the configured playlist.

`privacy_status` and `playlist` default to your `yb` config (see
[`yb.config`](_autosummary/yb.config.html.md#module-yb.config)): unset means “use the config value”, so privacy falls back
to `unlisted` and the video joins your configured playlist. Pass
`playlist=None` to skip the playlist for one call, or `playlist="Name"`
to override the target.

`cred_kwargs` reach [`get_credentials()`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_credentials) (e.g.
`open_browser=False, port=8080` for headless consent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  `{"video_id", "url", "studio_url", "privacy_status", "captions",
  "thumbnail", "playlist"}`. `privacy_status` reflects what YouTube
  actually set (an unaudited project may force `"private"`); `playlist`
  is the [`add_video_to_playlist()`](_autosummary/yb.youtube.playlists.html.md#yb.youtube.playlists.add_video_to_playlist) result or
  `None`.
* **Raises:**
  [**PartialPublish**](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.PartialPublish) – the upload succeeded and a later step failed;
      `err.result` holds the video id and what was done. Pass
      `resume=err.result` to finish without uploading again (yb#21).

### yb.youtube.publish.publish_video(video_path, metadata, \*, privacy_status=None, playlist=<object object>, captions=None, thumbnail=None, config=None, client_secrets_file=None, token_file=None, progress=True, service=None, resume=None, \*\*cred_kwargs)

Lower-level upload: explicit `VideoMetadata` + caption tracks.

`privacy_status` and `playlist` default to your `yb` config (see
[`publish_content()`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.publish_content)).

If a step after the upload fails, [`PartialPublish`](_autosummary/yb.youtube.publish.html.md#yb.youtube.publish.PartialPublish) is raised with
the new video’s id and what was done so far (`err.result`). Pass
`resume=err.result` to finish without uploading again (yb#21).

`cred_kwargs` reach [`get_credentials()`](_autosummary/yb.youtube.auth.html.md#yb.youtube.auth.get_credentials) (e.g.
`open_browser=False, port=8080` for headless consent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/yb.youtube.search.html.md

# yb.youtube.search

Search YouTube for videos, with engagement numbers to rank them by.

A read-only companion to [`yb.youtube.stats`](_autosummary/yb.youtube.stats.html.md#module-yb.youtube.stats): that module tells you about a
video you already know; this one finds the videos.

Simple things simple:

```default
>>> from yb.youtube import search_videos
>>> for v in search_videos("prime factorisation", max_results=3):
...     print(v["views"], v["title"])
```

Two backends, because the obvious one is not always available:

`"api"`
: The official Data API `search.list`. Costs 100 quota units per call out of
  a default 10 000/day, so roughly 100 searches. Needs `GOOGLE_API_KEY` *and*
  YouTube Data API v3 enabled on that key’s Cloud project.

`"scrape"`
: Reads the public results page and pulls the same public fields out of the
  `ytInitialData` blob the page ships to render itself. No key, no quota. It
  depends on a page layout Google can change, so it is the fallback, not the
  default — and it is for occasional personal use, not bulk collection.

`backend="auto"` (the default) uses the API when it can and falls back
otherwise, telling you which it used in each result’s `source` field, so a
silently degraded search is never mistaken for a good one.

Search results carry no engagement numbers on either backend. Pass
`with_stats=True` to fill in `views`/`likes`/`comments` — one extra
batched call per 50 videos on the API backend (1 quota unit), or one page fetch
per video when scraping.

**What each backend can actually tell you about engagement** (measured, not
assumed): the API gives views, likes and comments. The scrape gives \*\*views
only\*\* – YouTube no longer ships the like count in the watch page HTML for a
signed-out request, so `likes` comes back `None` there rather than wrong.
[`rank_by_engagement()`](_autosummary/yb.youtube.search.html.md#yb.youtube.search.rank_by_engagement) degrades to ranking on reach alone when likes are
missing; enabling the Data API is what makes approval-weighted ranking possible.

### Module Attributes

| [`WATCH_URL`](_autosummary/yb.youtube.search.html.md#yb.youtube.search.WATCH_URL)           | Where a video lives, and the thumbnail YouTube always serves for it.   |
|----------------------------------------------------------------------|------------------------------------------------------------------------|
| [`THUMBNAIL_QUALITIES`](_autosummary/yb.youtube.search.html.md#yb.youtube.search.THUMBNAIL_QUALITIES) | Thumbnail sizes YouTube serves for every video without an API call.    |

### Functions

| [`add_stats`](_autosummary/yb.youtube.search.html.md#yb.youtube.search.add_stats)(videos, \*[, api_key, backend])        | Fill in engagement numbers on search results, in place-ish (returns them).   |
|---------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`api_available`](_autosummary/yb.youtube.search.html.md#yb.youtube.search.api_available)(\*[, api_key])                     | Whether the Data API can actually be called with the key we have.            |
| [`rank_by_engagement`](_autosummary/yb.youtube.search.html.md#yb.youtube.search.rank_by_engagement)(videos, \*[, min_views, ...]) | Order videos by a like-rate-and-reach score.                                 |
| [`search_videos`](_autosummary/yb.youtube.search.html.md#yb.youtube.search.search_videos)(query, \*[, max_results, ...])     | Search YouTube and return a list of video records.                           |
| [`thumbnail_url`](_autosummary/yb.youtube.search.html.md#yb.youtube.search.thumbnail_url)(video_id, \*[, quality])           | Thumbnail URL for a video.                                                   |
| `watch_url`(video_id)                                                                             |                                                                              |

### yb.youtube.search.THUMBNAIL_QUALITIES *= ('default', 'mqdefault', 'hqdefault', 'sddefault', 'maxresdefault')*

Thumbnail sizes YouTube serves for every video without an API call.

### yb.youtube.search.WATCH_URL *= 'https://www.youtube.com/watch?v={video_id}'*

Where a video lives, and the thumbnail YouTube always serves for it.

### yb.youtube.search.add_stats(videos, , api_key=None, backend='auto')

Fill in engagement numbers on search results, in place-ish (returns them).

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### yb.youtube.search.api_available(, api_key=None)

Whether the Data API can actually be called with the key we have.

A key that exists is not a key that works: the API has to be enabled on its
Cloud project too, and that failure is a 403 at call time rather than
anything visible up front.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### yb.youtube.search.rank_by_engagement(videos, , min_views=0, like_weight=1.0, view_weight=1.0)

Order videos by a like-rate-and-reach score.

Raw view count alone rewards age and luck, and like count alone rewards the
same. The score multiplies *reach* (log views, so an order of magnitude
counts for a fixed amount) by *approval* (likes per thousand views), which
ranks a well-liked mid-sized video above a big indifferent one. Videos
missing numbers sort last rather than being dropped, since absent is not the
same as bad.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### yb.youtube.search.search_videos(query, , max_results=10, backend='auto', with_stats=False, api_key=None, language=None, region=None, \*\*params)

Search YouTube and return a list of video records.

`language`/`region` bias results (`"en"`, `"GB"`) rather than filter
them — YouTube offers no hard language filter. Check what comes back if the
language matters; a title can be auto-translated by the interface, which
makes an English video look like a local one.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)

### yb.youtube.search.thumbnail_url(video_id, , quality='mqdefault')

Thumbnail URL for a video. Always available, no API call, no key.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/yb.youtube.stats.html.md

# yb.youtube.stats

Read live video metadata & engagement numbers from the YouTube Data API v3.

A read-only companion to [`yb.youtube.api`](_autosummary/yb.youtube.api.html.md#module-yb.youtube.api): fetch a flattened, typed,
human-friendly view of a video’s *live* numbers (views, likes, dislikes,
comments, …) together with content and status details — all in one request —
with optional field selection, named field **groups** (presets like
`"engagement"`), and an ASCII-table rendering for quick terminal reading.

Simple things simple:

```default
>>> from yb.youtube import video_metadata
>>> video_metadata("VIDEO_ID", group="engagement")
{'views': 666, 'likes': 11, 'dislikes': 0, 'comments': 0, ...}
```

Readable table for a terminal:

```default
>>> print(video_metadata("VIDEO_ID", group="engagement", as_table=True))
field              value
-----------------  -----
views                666
likes                 11
...
```

Pick exact fields and order, or compare several videos at once:

```default
>>> video_metadata("VIDEO_ID", fields=["title", "views", "likes"])
>>> print(video_metadata(["ID1", "ID2"], group="engagement", as_table=True))
```

#### NOTE
`dislikes` is only returned by the API to a video’s **owner**; for
other people’s videos it comes back as `None`. With no `group`/`fields` you
get every available field (“take whatever is there”).

### Module Attributes

| [`DEFAULT_PARTS`](_autosummary/yb.youtube.stats.html.md#yb.youtube.stats.DEFAULT_PARTS)   | Parts fetched by default — everything a single `videos.list` call needs to populate the flattened fields below.   |
|------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| [`FIELD_GROUPS`](_autosummary/yb.youtube.stats.html.md#yb.youtube.stats.FIELD_GROUPS)    | a group name -> the ordered fields it selects.                                                                    |

### Functions

| [`flatten_video`](_autosummary/yb.youtube.stats.html.md#yb.youtube.stats.flatten_video)(resource)                            | Flatten a raw `videos.list` item into a friendly, typed, ordered dict.   |
|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| [`render_table`](_autosummary/yb.youtube.stats.html.md#yb.youtube.stats.render_table)(data, \*[, fields])                   | Render metadata as an ASCII table.                                       |
| [`resolve_fields`](_autosummary/yb.youtube.stats.html.md#yb.youtube.stats.resolve_fields)(\*[, group, fields, available])     | Resolve the ordered field list to show.                                  |
| [`select_fields`](_autosummary/yb.youtube.stats.html.md#yb.youtube.stats.select_fields)(flat, \*[, group, fields])           | Return an ordered subset of `flat` per `fields`/`group`.                 |
| [`video_metadata`](_autosummary/yb.youtube.stats.html.md#yb.youtube.stats.video_metadata)(video_id, \*[, group, fields, ...]) | Fetch a video's live metadata & engagement numbers.                      |

### yb.youtube.stats.DEFAULT_PARTS *= 'snippet,statistics,contentDetails,status'*

Parts fetched by default — everything a single `videos.list` call needs to
populate the flattened fields below.

### yb.youtube.stats.FIELD_GROUPS *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]]* *= {'content': ['duration', 'duration_seconds', 'definition', 'dimension', 'has_captions', 'has_custom_thumbnail', 'licensed_content', 'projection'], 'engagement': ['views', 'likes', 'dislikes', 'comments', 'favorites', 'like_view_pct', 'comment_view_pct'], 'identity': ['id', 'url', 'title', 'channel_title', 'channel_id', 'category_id', 'published_at'], 'overview': ['title', 'url', 'privacy', 'published_at', 'duration', 'views', 'likes', 'comments'], 'status': ['privacy', 'upload_status', 'made_for_kids', 'embeddable', 'license', 'public_stats_viewable']}*

a group name -> the ordered fields it selects. `"engagement"`
is the headline one (the live numbers); the rest are common practical cuts.

* **Type:**
  Named presets

### yb.youtube.stats.flatten_video(resource)

Flatten a raw `videos.list` item into a friendly, typed, ordered dict.

Pure: pass the dict returned by the API. Missing pieces (from a partial
`part=` request, disabled stats, or an unauthorized dislike count) become
`None` rather than raising, so callers never have to guard the shape.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.stats.render_table(data, , fields=None)

Render metadata as an ASCII table.

A single flat dict renders as a two-column `field | value` table; a list
of flat dicts renders one row per video with `fields` as columns.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### yb.youtube.stats.resolve_fields(, group=None, fields=None, available=None)

Resolve the ordered field list to show.

Precedence: explicit `fields` > named `group` > every `available`
field. An unknown `group` raises `KeyError` naming the valid options.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### yb.youtube.stats.select_fields(flat, , group=None, fields=None)

Return an ordered subset of `flat` per `fields`/`group`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### yb.youtube.stats.video_metadata(video_id, , group=None, fields=None, part='snippet,statistics,contentDetails,status', as_table=False, service=None, \*\*cred_kwargs)

Fetch a video’s live metadata & engagement numbers.

* **Parameters:**
  * **video_id** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – A single video id or an iterable of ids (batched, ≤50/call).
  * **group** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Name of a preset field set from [`FIELD_GROUPS`](_autosummary/yb.youtube.stats.html.md#yb.youtube.stats.FIELD_GROUPS)
    (e.g. `"engagement"`). Ignored if `fields` is given.
  * **fields** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – Explicit ordered field names to keep (overrides `group`).
    With neither `group` nor `fields` you get every field.
  * **part** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – `videos.list` parts to request (default covers all fields).
  * **as_table** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When `True`, return a ready-to-print ASCII table string
    instead of the dict/list.
  * **service** – An authenticated YouTube service; else built from
    `cred_kwargs` (e.g. `token_file=...`).
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)] | [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  A flat `dict` for one id / `list[dict]` for many — or an ASCII
  table `str` when `as_table=True`.


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-10-14 12:40 UTC** from commit <a href="https://github.com/reeleehq/yb/commit/90625947263aaaee33fc256e400c8ce8727288be"><code>9062594</code></a> on branch <code>main</code>, for **yb 0.1.19** (from <code>pyproject.toml</code>).

#### WARNING
The documentation and the package may be misaligned:

- The documented version (0.1.19) is behind the latest release on PyPI (0.1.20): `pip install yb` gives newer code than these docs describe.

## Source

|                     |                                                                                                                                                    |
|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/reeleehq/yb/commit/90625947263aaaee33fc256e400c8ce8727288be"><code>90625947263aaaee33fc256e400c8ce8727288be</code></a> |
| Branch              | <code>main</code>                                                                                                                                  |
| Tags at this commit | none                                                                                                                                               |
| Working tree        | clean                                                                                                                                              |
| Remote              | <code>https://github.com/reeleehq/yb</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>reeleehq/yb</code>                                                                   |
| Run          | <a href="https://github.com/reeleehq/yb/actions/runs/38565400673">38565400673</a>          |
| Ref          | <code>refs/heads/main</code>                                                               |
| Event commit | <code>90625947263aaaee33fc256e400c8ce8727288be</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.17  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.15 |

## Configuration as resolved

|               |                                                                  |
|---------------|------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>furo</code>)               |
| accent        | <code>#006787</code>                                             |
| api_generator | <code>autosummary</code>                                         |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code> |
| agent_outputs | <code>true</code>                                                |
| aggregates    | <code>md</code>                                                  |
| ai_artifacts  | <code>true</code>                                                |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/yb/0.1.20/">0.1.20</a>, newer than the documented version (0.1.19).

## Reproduce

```bash
git clone https://github.com/reeleehq/yb && cd yb
git checkout 90625947263aaaee33fc256e400c8ce8727288be
pip install "epythet==0.2.17"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# ai-agents.html.md

<!-- generated by epythet -->

# For AI agents

`yb` ships artifacts for coding agents alongside its code. This page lists
them, says where each lives in the repository, and points at the
machine-readable copies of this documentation.

## Skills

Skills are folders holding a `SKILL.md` (the [Agent Skills](https://agentskills.io) format): a description that tells an agent when to use it and a body with the procedure. Install one into your agent with `gh skill` (any host: `--agent claude-code`, `copilot`, `cursor`, `codex`, `gemini`), or use the copy bundled in the wheel.

### `music2video`

Turn songs (.wav/.mp3) into publishable YouTube music videos with the `yb` package: render a still-cover, Ken Burns, or audio-reactive video, derive the thumbnail, loudness-normalize a whole album, verify the result against what YouTube actually accepts, and publish. Use when the user wants to publish a song or album to YouTube, make a music video or visualizer from an audio file, add cover art to a track for upload, or check a rendered music video before shipping it.

```bash
gh skill install thorwhalen/yb music2video --agent claude-code
```

Source: [`yb/data/skills/music2video`](https://github.com/thorwhalen/yb/tree/HEAD/yb/data/skills/music2video) (bundled with the pip package).

### `yb-download`

Download a YouTube video, or just its audio, with the `yb` package — including metadata-only lookups, playlists, sidecars (subtitles/thumbnail/description), and converting the downloaded audio to a specific format (mp3, wav, …). Use when the user wants to get/fetch/grab/rip a video or its audio from a YouTube link, extract audio from a video URL, pull a video’s metadata without downloading, or convert an audio file’s format.

```bash
gh skill install thorwhalen/yb yb-download --agent claude-code
```

Source: [`yb/data/skills/yb-download`](https://github.com/thorwhalen/yb/tree/HEAD/yb/data/skills/yb-download) (bundled with the pip package).

### `yb-podcast`

Prepare a podcast episode with the `yb` package: show notes, chapter markers (ID3 chapters embedded in the MP3 + a Podlove PSC sidecar), an optional cover-over-audio video (static or Ken Burns), and an RSS episode item. Use when the user wants to turn audio (or a video’s audio) into a publishable podcast episode, generate show notes / podcast chapters, embed chapters in an MP3, make a cover video for an audio episode, or build a podcast RSS feed.

```bash
gh skill install thorwhalen/yb yb-podcast --agent claude-code
```

Source: [`yb/data/skills/yb-podcast`](https://github.com/thorwhalen/yb/tree/HEAD/yb/data/skills/yb-podcast) (bundled with the pip package).

### `yb-publish`

Prepare and publish a video to YouTube with the `yb` package: transcribe, derive title/description/keywords, detect chapters, render a thumbnail, upload (with captions), and edit already-published videos (metadata, subtitles, chapters, thumbnail). Use when the user wants to publish/upload a video to YouTube, prepare YouTube metadata/chapters/subtitles, replace or add subtitles on an existing video, or update a published video’s title/ description/tags/thumbnail. For first-time OAuth setup, use yb-setup first.

```bash
gh skill install thorwhalen/yb yb-publish --agent claude-code
```

Source: [`yb/data/skills/yb-publish`](https://github.com/thorwhalen/yb/tree/HEAD/yb/data/skills/yb-publish) (bundled with the pip package).

### `yb-setup`

Set up YouTube publishing for the `yb` package: install the right extras, create a Google OAuth client (gcloud optional — the console path works), wire up the credentials, run the one-time consent, and test the connection. Use when the user wants to publish/upload to YouTube for the first time, hits an OAuth or “client secrets” error, sees “accessNotConfigured”, needs to make or replace a YouTube OAuth client, or asks how to authenticate yb with YouTube.

```bash
gh skill install thorwhalen/yb yb-setup --agent claude-code
```

Source: [`yb/data/skills/yb-setup`](https://github.com/thorwhalen/yb/tree/HEAD/yb/data/skills/yb-setup) (bundled with the pip package).

The bundled skills are also on disk after `pip install yb`, under the package’s `data/skills/` directory; link them into an agent without network access with `skill link-skills <that directory>`.

## Machine-readable documentation

This site publishes the same documentation in forms that fit an agent’s context window:

- [`llms.txt`](https://thorwhalen.github.io/yb/llms.txt): an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)
- [`yb.md`](https://thorwhalen.github.io/yb/yb.md): the whole documentation as one Markdown file
- `<page>.html.md`: a rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`
- [`objects.inv`](https://thorwhalen.github.io/yb/objects.inv): the Sphinx inventory: a symbol-to-URL index (`sphobjinv convert plain objects.inv -`)


# api.html.md

# API reference

| [`yb`](_autosummary/yb.html.md#module-yb)   | yb — streamline media publishing: prepare once, publish to YouTube or podcast.   |
|-----------------------------------------------------------------|----------------------------------------------------------------------------------|


