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:
>>> 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:
>>> 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
|
Whether the Data API can actually be called with the key we have. |
|
|
|
Thumbnail URL for a video. |
|
Order videos by a like-rate-and-reach score. |
|
Fill in engagement numbers on search results, in place-ish (returns them). |
|
Search YouTube and return a list of video records. |
|
Begin paste-back consent and return the URL to open. |
|
Complete a consent begun by |
|
Return OAuth user credentials, running the consent flow if needed. |
|
Build a YouTube Data API v3 service object. |
Cached OAuth token location ( |
|
Default OAuth client-secrets location: |
|
|
Return all playlists owned by the authenticated channel (paginated). |
|
Return the id of the caller's playlist titled |
|
Create a playlist and return its id. |
|
Return the id of the playlist titled |
|
Whether |
|
Add |
|
Find-or-create the playlist named |
|
The |
|
The 0-based position of |
|
Ids of the authenticated channel's playlists that hold |
|
Remove every occurrence of |
|
Change a video's privacy (or synthetic-media flag), keeping the rest. |
|
Retire an upload that |
|
Fetch a video resource (raises |
|
Resumably upload a video with the given |
|
Update a video's snippet ( |
|
Patch selected snippet fields, preserving the rest. |
|
Set a custom thumbnail ( |
|
Insert/replace a chapters block in the video's description. |
|
Fetch a video's live metadata & engagement numbers. |
|
Flatten a raw |
|
Return an ordered subset of |
|
Resolve the ordered field list to show. |
|
Render metadata as an ASCII table. |
|
List caption tracks on a video (each item's snippet has language/name/trackKind/status). |
|
Insert a new caption track ( |
|
Replace the content of an existing caption track ( |
|
Insert a caption track, or update the existing same-language one. |
|
Download a caption track's content ( |
|
Delete a caption track ( |
|
Upload a prepared |
|
One call: prepare publication content from |
|
Lower-level upload: explicit |
Classes
|
YouTube-specific video metadata. |
|
A caption track to attach to a video. |
Exceptions
Consent is needed and this process cannot obtain it. |
|
|
Consent was started, and now waits for the user to paste the redirect back. |
|
|
|
The video was uploaded, then a later step failed (yb#21). |
- class yb.youtube.CaptionTrack(path, language, name='', is_draft=False)[source]¶
Bases:
objectA 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)[source]¶
Bases:
ConsentRequiredConsent 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.
urlis the address to open; the message says what to do with the result. SubclassesConsentRequiredso a caller that only handles “no credentials” still stops, while one that knows about paste mode can catch this and relayurl.
- exception yb.youtube.ConsentRequired[source]¶
Bases:
RuntimeErrorConsent 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_serverprints a URL to a stdout nobody is reading and waits onlocalhostfor 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 ofinvalid_grant.
- exception yb.youtube.PartialPublish(result, *, step, error)[source]¶
Bases:
RuntimeErrorThe 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,captionsattached 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.resultto the same call (publish_video()orpublish_content(); forprepare_and_publish(), callpublish_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 savedvideo_id.resultis 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)[source]¶
Bases:
objectYouTube-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)[source]¶
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:
- exception yb.youtube.VisibilityDropWarning[source]¶
Bases:
UserWarningsupersede()leaves the re-cut less visible than the original was.Emitted before anything is changed, so escalating it refuses the call:
warnings.simplefilter("error", VisibilityDropWarning)
- yb.youtube.add_stats(videos, *, api_key=None, backend='auto')[source]¶
Fill in engagement numbers on search results, in place-ish (returns them).
- Return type:
- yb.youtube.add_to_playlist(video_id, playlist_id, *, position=None, service=None, **cred_kwargs)[source]¶
Add
video_idtoplaylist_id(playlistItems.insert).Appended at the end, or inserted at the 0-based
positionwhen given (YouTube refuses a position on a playlist not sorted manually).- Return type:
- yb.youtube.add_video_to_playlist(video_id, title, *, create=True, privacy_status='private', skip_if_present=True, service=None, **cred_kwargs)[source]¶
Find-or-create the playlist named
titleand appendvideo_id.Idempotent: with
skip_if_present(default), a video already in the playlist is left alone instead of duplicated.
- yb.youtube.api_available(*, api_key=None)[source]¶
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:
- yb.youtube.create_playlist(title, *, description='', privacy_status='private', service=None, **cred_kwargs)[source]¶
Create a playlist and return its id.
- Return type:
- yb.youtube.default_client_secrets_file()[source]¶
Default OAuth client-secrets location:
<config dir>/client_secret.json.Used when neither
client_secrets_file=nor the env vars are set, so all ofyb’s state can live in one directory next to the token.- Return type:
- yb.youtube.default_token_file()[source]¶
Cached OAuth token location (
$XDG_CONFIG_HOMEor~/.config).- Return type:
- yb.youtube.delete_caption(caption_id, *, service=None, **cred_kwargs)[source]¶
Delete a caption track (
captions.delete).- Return type:
- yb.youtube.download_caption(caption_id, *, tfmt='srt', service=None, **cred_kwargs)[source]¶
Download a caption track’s content (
captions.download), default SRT.- Return type:
- yb.youtube.ensure_playlist(title, *, create=True, description='', privacy_status='private', service=None, **cred_kwargs)[source]¶
Return the id of the playlist titled
title, creating it if missing.Returns
Noneonly when the playlist is absent andcreate=False.
- yb.youtube.find_playlist(title, *, service=None, **cred_kwargs)[source]¶
Return the id of the caller’s playlist titled
title(orNone).Matches the first playlist with an exact title; YouTube allows duplicate titles, so prefer unique playlist names.
- yb.youtube.finish_paste_consent(authorization_response, *, client_secrets_file=None, pending_file=None)[source]¶
Complete a consent begun by
start_paste_consent(); return credentials.authorization_responseis the redirected URL, its query string, or the barecode. 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)[source]¶
The 0-based position of
video_id’s first occurrence (Noneif absent).
- yb.youtube.flatten_video(resource)[source]¶
Flatten a raw
videos.listitem 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) becomeNonerather than raising, so callers never have to guard the shape.- Return type:
- 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)[source]¶
Return OAuth user credentials, running the consent flow if needed.
First use runs the installed-app consent flow and caches the token to
token_fileso later calls are non-interactive. Expired tokens are refreshed automatically.Pass
interactive=Falsewhen 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 raisesConsentRequirednaming 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=Truethe call can no longer hang: consent is bounded bytimeout_seconds(DEFAULT_CONSENT_TIMEOUT_S), and a timeout is reported as the sameConsentRequired, 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(0picks a free one), because Google retired the copy-paste “out-of-band” flow in 2022.open_browser=Falseonly 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=Falseand a fixedport=, 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 anylocalhostport, which is also why theport=0default works.The call blocks until the redirect arrives, bounded by
timeout_seconds(defaultDEFAULT_CONSENT_TIMEOUT_S; passNonefor 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 raisingConsentPendingcarrying the URL to open (anywhere — a phone will do); the browser’s redirect tolocalhostfails to load, and the second call passes that address-bar URL (or just itscode) asauthorization_response=to finish and cache the token. The two calls may be separate turns or separate processes;yb authis the command-line form.portonly names the (unreachable) redirect.These keywords ride
**cred_kwargsthroughget_service()and the publishing helpers. The few entry points that take none (notablyyb.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)[source]¶
Build a YouTube Data API v3 service object.
cred_kwargsare forwarded toget_credentials()(token_file=,open_browser=,port=, …) unlesscredentialsis given.
- yb.youtube.get_video(video_id, *, part='snippet,status', service=None, **cred_kwargs)[source]¶
Fetch a video resource (raises
KeyErrorif not found/visible).- Return type:
- yb.youtube.insert_caption(video_id, path, *, language, name='', is_draft=False, service=None, **cred_kwargs)[source]¶
Insert a new caption track (
captions.insert).- Return type:
- yb.youtube.is_video_in_playlist(video_id, playlist_id, *, service=None, **cred_kwargs)[source]¶
Whether
video_idis already an item ofplaylist_id(paginated).- Return type:
- yb.youtube.list_captions(video_id, *, service=None, **cred_kwargs)[source]¶
List caption tracks on a video (each item’s snippet has language/name/trackKind/status).
- yb.youtube.list_my_playlists(*, service=None, **cred_kwargs)[source]¶
Return all playlists owned by the authenticated channel (paginated).
- yb.youtube.playlist_item_ids(video_id, playlist_id, *, service=None, **cred_kwargs)[source]¶
The
playlistItemsids holdingvideo_idinplaylist_id.A list, because a video can sit in one playlist more than once.
- yb.youtube.playlists_containing(video_id, *, service=None, **cred_kwargs)[source]¶
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.listpages: on a large channel, budget for it.
- 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)[source]¶
One call: prepare publication content from
mediaand 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_statusandplaylistdefault to yourybconfig (seepublish_content()).cred_kwargsreachget_credentials()(e.g.open_browser=False, port=8080for headless consent).- Return type:
- 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)[source]¶
Upload a prepared
PublicationContentto YouTube.Maps the content to a YouTube snippet (chapters embedded in the description when present), uploads, attaches the SRT caption track (language =
content.audio_languageorcontent.language) and the thumbnail when available, and adds the video to the configured playlist.privacy_statusandplaylistdefault to yourybconfig (seeyb.config): unset means “use the config value”, so privacy falls back tounlistedand the video joins your configured playlist. Passplaylist=Noneto skip the playlist for one call, orplaylist="Name"to override the target.cred_kwargsreachget_credentials()(e.g.open_browser=False, port=8080for headless consent).- Return type:
- Returns:
{"video_id", "url", "studio_url", "privacy_status", "captions", "thumbnail", "playlist"}.privacy_statusreflects what YouTube actually set (an unaudited project may force"private");playlistis theadd_video_to_playlist()result orNone.- Raises:
PartialPublish – the upload succeeded and a later step failed;
err.resultholds the video id and what was done. Passresume=err.resultto 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)[source]¶
Lower-level upload: explicit
VideoMetadata+ caption tracks.privacy_statusandplaylistdefault to yourybconfig (seepublish_content()).If a step after the upload fails,
PartialPublishis raised with the new video’s id and what was done so far (err.result). Passresume=err.resultto finish without uploading again (yb#21).cred_kwargsreachget_credentials()(e.g.open_browser=False, port=8080for headless consent).- Return type:
- yb.youtube.rank_by_engagement(videos, *, min_views=0, like_weight=1.0, view_weight=1.0)[source]¶
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:
- yb.youtube.remove_from_playlist(video_id, playlist_id, *, service=None, **cred_kwargs)[source]¶
Remove every occurrence of
video_idfromplaylist_id.Returns how many items were removed (0 when it was not there).
- Return type:
- yb.youtube.render_table(data, *, fields=None)[source]¶
Render metadata as an ASCII table.
A single flat dict renders as a two-column
field | valuetable; a list of flat dicts renders one row per video withfieldsas columns.- Return type:
- yb.youtube.resolve_fields(*, group=None, fields=None, available=None)[source]¶
Resolve the ordered field list to show.
Precedence: explicit
fields> namedgroup> everyavailablefield. An unknowngroupraisesKeyErrornaming the valid options.
- yb.youtube.search_videos(query, *, max_results=10, backend='auto', with_stats=False, api_key=None, language=None, region=None, **params)[source]¶
Search YouTube and return a list of video records.
language/regionbias 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:
- yb.youtube.select_fields(flat, *, group=None, fields=None)[source]¶
Return an ordered subset of
flatperfields/group.- Return type:
- yb.youtube.set_chapters(video_id, chapters, *, header='Chapters:', service=None, **cred_kwargs)[source]¶
Insert/replace a chapters block in the video’s description.
Strips any prior block under
headerand appends the new one. YouTube renders interactive chapters when the first timestamp is0:00.- Return type:
- yb.youtube.set_thumbnail(video_id, image_path, *, service=None, **cred_kwargs)[source]¶
Set a custom thumbnail (
thumbnails.set).- Return type:
- 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)[source]¶
Begin paste-back consent and return the URL to open.
A consent already pending for the same client,
scopesandportis reused (reuse_pending=True): re-printing the URL must not invalidate the one the user already has open on their phone.- Return type:
- yb.youtube.supersede(old_id, new_id, *, old_privacy='private', new_privacy=None, move_playlists=True, service=None, **cred_kwargs)[source]¶
Retire an upload that
new_idreplaces (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:
refuses, before changing anything, unless
new_idhas finished processing (uploadStatus == "processed"): retiring the old video for a failed or rejected upload would leave the public neither;sets
new_idtonew_privacywhen given (e.g."public");for every playlist of yours that holds
old_id(whenmove_playlists): insertsnew_idat the old one’s position (or appends, if the playlist is not sorted manually), THEN removesold_id, so a failure part-way never leaves a playlist with neither;sets
old_idtoold_privacy(default"private"), clearing any scheduled publish. It is never deleted.
The new video keeps its own privacy unless
new_privacyis given, and an upload isunlistedby 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), aVisibilityDropWarningis emitted before anything changes; passnew_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()). Safe to re-run: every step checks before it acts. Quota: seeyb.youtube.playlists_containing(); each write (insert, delete, update) is 50 units.- Return type:
- Returns:
{"old_id", "new_id", "playlists", "old_privacy", "new_privacy"}whereplaylistslists{"playlist_id", "added_new", "position", "removed_old"}.
- yb.youtube.thumbnail_url(video_id, *, quality='mqdefault')[source]¶
Thumbnail URL for a video. Always available, no API call, no key.
- Return type:
- yb.youtube.update_caption(caption_id, path, *, is_draft=None, service=None, **cred_kwargs)[source]¶
Replace the content of an existing caption track (
captions.update).- Return type:
- yb.youtube.update_video(video_id, snippet, *, service=None, **cred_kwargs)[source]¶
Update a video’s snippet (
videos.update).categoryIdis required.- Return type:
- 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)[source]¶
Patch selected snippet fields, preserving the rest.
Fetches the current snippet, overlays the provided fields, and updates.
categoryIdmust be present (kept from the existing snippet if not given).- Return type:
- yb.youtube.update_video_status(video_id, *, privacy_status=None, contains_synthetic_media=None, keep_schedule=False, service=None, **cred_kwargs)[source]¶
Change a video’s privacy (or synthetic-media flag), keeping the rest.
Read-modify-write of the
statuspart (yb#19):videos.updatereplaces 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_statusclears a scheduled publish (publishAt): otherwise a video made private while scheduled would still go public at that time, and a pastpublishAtread back is refused by the API. Passkeep_schedule=Trueto keep it (only meaningful with"private").- Raises:
ValueError –
privacy_statusis not one ofPRIVACY_STATUSES.- Return type:
- yb.youtube.upload_video(video_path, body, *, service=None, chunksize=8388608, progress=True, **cred_kwargs)[source]¶
Resumably upload a video with the given
videos.insertbody.Returns the created video resource (includes
id).- Return type:
- yb.youtube.upsert_caption(video_id, track, *, language=None, name='', is_draft=False, replace=True, service=None, **cred_kwargs)[source]¶
Insert a caption track, or update the existing same-language one.
- Parameters:
video_id (
str) – Target video.track (
CaptionTrack|str|Path) – ACaptionTrack, or a path (thenlanguagerequired).replace (
bool) – When an uploaded track in the same language exists, update it (True, default) rather than inserting a duplicate.
- Return type:
- 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)[source]¶
Fetch a video’s live metadata & engagement numbers.
- Parameters:
video_id (
Union[str,Iterable[str]]) – A single video id or an iterable of ids (batched, ≤50/call).group (
str|None) – Name of a preset field set fromFIELD_GROUPS(e.g."engagement"). Ignored iffieldsis given.fields (
Optional[Sequence[str]]) – Explicit ordered field names to keep (overridesgroup). With neithergroupnorfieldsyou get every field.part (
str) –videos.listparts to request (default covers all fields).as_table (
bool) – WhenTrue, 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:
- Returns:
A flat
dictfor one id /list[dict]for many — or an ASCII tablestrwhenas_table=True.
Modules
YouTube video operations: get, upload, update metadata, thumbnail, chapters. |
|
OAuth 2.0 plumbing for the YouTube Data API v3. |
|
Caption (subtitle) tracks on YouTube videos: list, insert, update, upsert. |
|
YouTube video metadata ↔ API snippet mapping. |
|
Paste-back OAuth consent: consent from a machine nobody is sitting at. |
|
YouTube playlist operations: find/create a playlist and add videos to it. |
|
High-level YouTube publishing: prepare → upload → captions → thumbnail. |
|
Search YouTube for videos, with engagement numbers to rank them by. |
|
Read live video metadata & engagement numbers from the YouTube Data API v3. |