Resumable upload of local video files to YouTube (used by the Vimeo migration and any new content), including thumbnail, initial metadata, and playlist assignment. Uploads default to private/unlisted pending review. Outward-facing: nothing is made public without explicit approval.
youtube.upload (insert) + youtube.force-ssl (set status/snippet) via bin/lib/youtube_client.rb (TASK-245).videos.insert?uploadType=resumable to get a session URI, then PUT the file in chunks; on interruption, re-PUT with Content-Range: bytes */TOTAL to query the last received byte and continue. faraday-multipart is available for the multipart metadata part.youtube_upload zdots job resumes instead of restarting. Fingerprint-idempotent job key = source file sha + target video_asset id.videos.insert sets status.privacyStatus = "private" (or unlisted). Videos are NEVER inserted public. Making a video public is a separate approved step: a youtube_metadata/status apply job (per TASK-245 two-phase gate) flips privacy only after explicit human approval per video.id and write it back into _data/video_assets.yml: add/append a platforms: entry { platform: youtube, asset_id: <new id>, url, embed_url } on the matching item (this is the one repo-write this pipeline owns; keep it minimal, structure-only). Downstream 248 (metadata) + 252 (captions) then target the new asset_id.videos.insert = 1600 units each. With the 10k/day default that is ~6 uploads/day; the 28 vimeo-primary (136 vimeo occurrences) migration set spans multiple days — batch across days or request a quota increase. Optional thumbnails.set (50) + playlist add handled in 258.videos.list → confirm privacyStatus=private and id matches the value written to _data/video_assets.yml; confirm a killed-and-resumed upload completes without a duplicate video.Writes: _data/video_assets.yml (new youtube asset_id only). Uses bin/lib/youtube_client.rb. Reads local master files (from TASK-246 Vimeo downloads / new content).