# YouTube private upload direct fallback

Use this when an AutoShorts/TrueTraceShorts private-upload package exists, the user has returned the exact hash-bound `APPROVED_FOR_PRIVATE_YOUTUBE_UPLOAD ...` command, but the repo-specific upload CLI is unavailable or has moved.

This is a fallback pattern, not the preferred path. Prefer the project CLI when present.

## Preconditions

- A `youtube_private_upload_package/*.json` exists for the candidate/version.
- The user approval command exactly matches the package `approval_command`.
- The package says `privacyStatus: private`.
- `video_path` exists and its SHA256 equals `video_sha256` from the package.
- The OAuth token has exactly one YouTube scope: `https://www.googleapis.com/auth/youtube.upload`.
- Do not call read/analytics/comment APIs. Upload-only means upload-only.
- If `thumbnail_path` is null or the user sets thumbnails manually, do not attempt `thumbnails.set`.

## Safe direct-upload sequence

1. Load the package JSON.
   - If the package was saved next to a render under `.../youtube_private_upload_package/` instead of the repo default, set `AUTOSHORTS_YOUTUBE_PACKAGE_DIR` to that directory before using `autoshorts.cli.youtube_private_upload_dry_run` or `autoshorts.cli.youtube_private_upload_execute`.
   - If the CLI fails with `unexpected keyword argument` for additive render metadata such as `music_bed`, patch `YouTubePrivateUploadPackage.from_json()` to ignore unknown JSON keys after preserving/validating the hash-bound fields; do not hand-edit the approval package or weaken hash validation.
2. Compare the returned approval command byte-for-byte/string-for-string with `approval_command`.
   - If the user replies with a natural-language approval such as `private upload approved` immediately after a single review package, do **not** invent a new approval string. Re-open the saved package, use its stored `approval_command`, and proceed only if it exactly matches the command that was just delivered in the prior review message and the dry-run gate is valid. If multiple packages or commands are in scope, ask for the exact command.
3. Abort unless `privacyStatus == "private"`.
4. Compute SHA256 of `video_path`; abort on mismatch.
5. Run the dry-run/validation path before the real upload; confirm title/description/tags, `privacyStatus=private`, `containsSyntheticMedia`, `selfDeclaredMadeForKids`, hash gate, and thumbnail path.
6. Load the token with `google.oauth2.credentials.Credentials` and verify its stored/effective scopes are exactly `{youtube.upload}`.
7. Refresh token if expired and refreshable; persist refreshed token with `0600` permissions, without printing secrets.
8. Build YouTube v3 client with `cache_discovery=False`.
9. Call only `videos.insert(part="snippet,status")` with:
   - `snippet.title`
   - `snippet.description`
   - `snippet.tags`
   - `snippet.categoryId`
   - `snippet.defaultLanguage`
   - `status.privacyStatus = private`
   - `status.selfDeclaredMadeForKids` from package
10. Upload MP4 via `MediaFileUpload(..., mimetype="video/mp4", resumable=True)`.
11. Write an audit JSON next to the package or in the repo upload-audit directory with:
    - timestamp
    - candidate/version
    - video SHA256
    - posting-pack SHA256
    - exact approval command
    - `privacyStatus: private`
    - `thumbnail_attempted: false` when appropriate
    - upload status
    - YouTube video ID
    - Studio URL
    - Watch URL
    - validated scope

## Partial failure rules

- If `videos.insert` succeeds but a later thumbnail step fails, do not rerun the upload; report the video ID/Studio URL and ask the user to set the thumbnail manually.
- If this fallback omits thumbnails by design, record `thumbnail_attempted: false` in the audit.
- Never compensate for a missing CLI by broadening OAuth scope or using read/analytics endpoints. That is not a workaround; that is how one summons the compliance goblin.

## Minimal implementation sketch

```python
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload

SCOPE = "https://www.googleapis.com/auth/youtube.upload"
creds = Credentials.from_authorized_user_file(str(token_path), scopes=[SCOPE])
if not creds.valid and creds.expired and creds.refresh_token:
    creds.refresh(Request())
    token_path.write_text(creds.to_json())
    os.chmod(token_path, 0o600)

assert set(creds.scopes or []) == {SCOPE}
youtube = build("youtube", "v3", credentials=creds, cache_discovery=False)
request = youtube.videos().insert(
    part="snippet,status",
    body={
        "snippet": {...},
        "status": {"privacyStatus": "private", "selfDeclaredMadeForKids": False},
    },
    media_body=MediaFileUpload(str(video_path), mimetype="video/mp4", resumable=True),
)
response = None
while response is None:
    status, response = request.next_chunk()
```
