@sulala/agent-os 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dashboard-dist/assets/index-BZYG7rCd.js +75 -0
  2. package/dashboard-dist/assets/index-CAOgf_FY.css +1 -0
  3. package/dashboard-dist/assets/index-CVI9FAmG.css +1 -0
  4. package/dashboard-dist/assets/index-DdMu_Z6v.js +75 -0
  5. package/dashboard-dist/index.html +2 -2
  6. package/data/agents/crm_hubspot_agent.json +11 -0
  7. package/data/agents/research_agent.json +1 -1
  8. package/data/agents/social_media_agent.json +1 -1
  9. package/data/agents/source_verify_agent.json +10 -0
  10. package/data/agents/writer_agent.json +1 -1
  11. package/data/skills/bluesky/SKILL.md +1 -1
  12. package/data/skills/content-writing/SKILL.md +32 -0
  13. package/data/skills/date/SKILL.md +1 -1
  14. package/data/skills/fetch/SKILL.md +1 -1
  15. package/data/skills/file-search/SKILL.md +1 -1
  16. package/data/skills/file-stats/SKILL.md +1 -1
  17. package/data/skills/git/SKILL.md +1 -1
  18. package/data/skills/hash/SKILL.md +1 -1
  19. package/data/skills/jq/SKILL.md +1 -1
  20. package/data/skills/markdown-to-html/SKILL.md +1 -1
  21. package/data/skills/qr-code/SKILL.md +1 -1
  22. package/data/skills/rss/SKILL.md +1 -1
  23. package/data/skills/translate/SKILL.md +1 -1
  24. package/data/skills/weather/SKILL.md +1 -1
  25. package/data/skills/web-search/SKILL.md +1 -1
  26. package/data/skills/webhook/SKILL.md +1 -1
  27. package/dist/cli.js +234 -69
  28. package/dist/index.js +168 -57
  29. package/package.json +1 -1
  30. package/data/skills/gmail/SKILL.md +0 -55
  31. package/data/skills/gmail/references/send-email.md +0 -54
  32. package/data/skills/gmail/scripts/send_email.py +0 -94
  33. package/data/skills/youtube/SKILL.md +0 -91
  34. package/data/skills/youtube/config.schema.json +0 -11
  35. package/data/skills/youtube/package.json +0 -8
  36. package/data/skills/youtube/references/youtube-upload.md +0 -65
  37. package/data/skills/youtube/requirements.txt +0 -3
  38. package/data/skills/youtube/scripts/youtube_upload.js +0 -200
  39. package/data/skills/youtube/scripts/youtube_upload.py +0 -125
@@ -1,91 +0,0 @@
1
- ---
2
- name: youtube
3
- description: Upload videos (or Shorts) to YouTube using a local OAuth setup. Uses a script in the skill directory; run it via the exec tool. Use when the user wants to upload a video, publish to YouTube, or upload Shorts.
4
- credentials:
5
- - YOUTUBE_CLIENT_SECRET_JSON
6
- ---
7
-
8
-
9
- # YouTube upload
10
-
11
- Upload videos to **YouTube** using a **script** in this skill. Credentials are stored in the skill directory (`client_secret.json` + `token.json`). Use the **exec** tool to run the script; no static upload code in the loader.
12
-
13
- ## Overview
14
-
15
- 1. **One-time setup**: Enable YouTube Data API v3, create an OAuth Desktop client, add the client JSON via Skills → Configure (or place `client_secret.json` in the skill). Install Node dependencies with **npm install** (see below). Run the upload script once to complete browser OAuth; `token.json` is saved for reuse.
16
- 2. **Upload**: Use **exec** with `skill_id: "youtube"` and a `command` that runs **node scripts/youtube_upload.js** (recommended) or the Python script, with `--file`, `--title`, and optional `--description`, `--tags`, `--privacy`.
17
-
18
- ## Setup
19
-
20
- Follow these steps so the skill can upload to your YouTube account. The **Skills → YouTube → Configure** dialog (Setup button) shows this section and a field for the client JSON.
21
-
22
- 1. **Google Cloud project**
23
- Go to [Google Cloud Console](https://console.cloud.google.com/). Create or select a project.
24
-
25
- 2. **Enable YouTube Data API v3**
26
- APIs & Services → Library → search “YouTube Data API v3” → Enable.
27
-
28
- 3. **Create OAuth 2.0 credentials**
29
- APIs & Services → Credentials → Create Credentials → **OAuth client ID**.
30
- - If asked, configure the OAuth consent screen (e.g. External, add your email).
31
- - Application type: **Web application** (so you can set a redirect URI).
32
- - Name: e.g. “YouTube upload”.
33
- - Under **Authorized redirect URIs**, click Add URI and add exactly:
34
- **`http://localhost:8090/`**
35
- (If this is missing, you will get “Error 400: redirect_uri_mismatch” when signing in.)
36
- - Create. Copy or download the client JSON.
37
-
38
- 4. **Add the client JSON to this skill**
39
- Paste the **full JSON** (the whole file content) into the **OAuth client JSON** field below (Skills → YouTube → Configure), then Save.
40
- Alternatively, save the file as `scripts/client_secret.json` in the YouTube skill directory.
41
-
42
- 5. **Install Node dependencies**
43
- From the skill directory run: `npm install`. The agent can do this via exec with `skill_id: "youtube"`, `command: "npm install"`.
44
-
45
- 6. **First sign-in**
46
- Run the upload script once (e.g. with a test video). A browser opens; sign in with your Google account and allow access. After that, `token.json` is saved and future uploads do not require signing in again.
47
-
48
- Details: [references/youtube-upload.md](references/youtube-upload.md).
49
-
50
- ## Install dependencies (agent must do this when needed)
51
-
52
- **Preferred (Node):** If the Node script fails with "Missing npm dependencies", or before first upload, install with **exec** and `skill_id: "youtube"`, `command: "npm install"`. Then retry. No system Python or pip needed.
53
-
54
- **Optional (Python):** If using the Python script and it reports missing Google API packages, use a virtual environment; the agent cannot install into system Python on externally-managed systems (e.g. macOS).
55
-
56
- ## Upload video (exec tool)
57
-
58
- Use **exec** with `skill_id: "youtube"` (command runs in the skill directory).
59
-
60
- **Recommended — Node script (no Python/pip):**
61
- `node scripts/youtube_upload.js --file <path> --title "<title>" [--description "<desc>"] [--tags "tag1,tag2"] [--privacy public|private|unlisted]`
62
-
63
- Example:
64
-
65
- ```json
66
- {
67
- "skill_id": "youtube",
68
- "command": "node scripts/youtube_upload.js --file /path/to/video.mp4 --title \"My video\" --description \"Optional\" --tags \"shorts,demo\" --privacy public"
69
- }
70
- ```
71
-
72
- **Optional — Python script** (requires venv + pip): `python3 scripts/youtube_upload.py --file ... --title ...`
73
-
74
- - **--file**: Path to the video file (absolute or relative to skill dir). Must be reachable where the agent runs.
75
- - **--title**: Required. **--description**, **--tags**, **--privacy** optional (default: `public`).
76
-
77
- **Interpreting exec result:** After running the upload script, check **stdout**. If it contains a line like `Uploaded: https://youtube.com/watch?v=...`, the upload **succeeded**. Tell the user the video was uploaded and give them that URL. Do not say authorization is required when stdout shows a success URL. If exitCode is non-zero but stdout contains the upload URL, still report success (the script may have been interrupted after writing the URL). Only when stdout has no upload URL and stderr says "Open this URL in your browser" should you ask the user to authorize.
78
-
79
- Full script options: [references/youtube-upload.md](references/youtube-upload.md).
80
-
81
- ## Skill layout
82
-
83
- - **scripts/youtube_upload.js** — Node upload script (recommended): `--file`, `--title`, `--description`, `--tags`, `--privacy`. No Python/pip required.
84
- - **scripts/youtube_upload.py** — Python upload script (optional; requires venv + pip install).
85
- - **package.json** — Node dependencies (googleapis). Run `npm install` in the skill dir.
86
- - **config.schema.json** — defines `YOUTUBE_CLIENT_SECRET_JSON` for the Skills config UI.
87
- - **scripts/client_secret.json** — (optional) OAuth client JSON if not set via Skills → Configure.
88
- - **scripts/token.json** — (created on first run) stored credentials; shared by both scripts.
89
- - **requirements.txt** — pip dependencies for the Python script only.
90
- - **references/youtube-upload.md** — setup, usage, and exec examples.
91
- - **SKILL.md** — this file.
@@ -1,11 +0,0 @@
1
- {
2
- "type": "object",
3
- "properties": {
4
- "YOUTUBE_CLIENT_SECRET_JSON": {
5
- "type": "string",
6
- "format": "password",
7
- "title": "OAuth client JSON",
8
- "description": "Paste the full content of client_secret.json from Google Cloud Console (Desktop OAuth 2.0 client, YouTube Data API v3 enabled). Alternatively place the file as scripts/client_secret.json in this skill directory."
9
- }
10
- }
11
- }
@@ -1,8 +0,0 @@
1
- {
2
- "name": "youtube-skill",
3
- "private": true,
4
- "description": "YouTube upload script dependencies",
5
- "dependencies": {
6
- "googleapis": "^144.0.0"
7
- }
8
- }
@@ -1,65 +0,0 @@
1
- # YouTube upload script
2
-
3
- Upload a video to YouTube using OAuth credentials. The script reads credentials from **Skills config** (env `YOUTUBE_CLIENT_SECRET_JSON`, set via Dashboard → Skills → YouTube → Configure) or from `scripts/client_secret.json`, and saves `token.json` after the first browser OAuth flow.
4
-
5
- ## Prerequisites
6
-
7
- - **Google Cloud project** with YouTube Data API v3 enabled.
8
- - **OAuth 2.0 Client** (Desktop app): add the JSON via **Skills → YouTube → Configure** (paste full content), or save as `data/skills/youtube/scripts/client_secret.json`.
9
- - **Python 3** with pip packages from `data/skills/youtube/requirements.txt`:
10
- ```bash
11
- pip install -r data/skills/youtube/requirements.txt
12
- ```
13
- Or: `google-api-python-client`, `google-auth-oauthlib`, `google-auth-httplib2`.
14
-
15
- ## One-time setup
16
-
17
- 1. In [Google Cloud Console](https://console.cloud.google.com/): create or select a project → APIs & Services → Enable **YouTube Data API v3**.
18
- 2. Create **OAuth 2.0 Client ID** (Application type: Desktop app). Download the JSON.
19
- 3. Add the JSON via **Skills → YouTube → Configure** (paste the full content), or save as `client_secret.json` in the skill’s **scripts** directory.
20
- 4. Run the script once with any test args; a browser will open to sign in. After authorizing, `token.json` is created in the same directory. Future runs use this token (refreshed automatically).
21
-
22
- ## Usage
23
-
24
- From the skill directory (or use **exec** with `skill_id: "youtube"` so cwd is the skill dir):
25
-
26
- ```bash
27
- python3 scripts/youtube_upload.py \
28
- --file /path/to/video.mp4 \
29
- --title "My video title" \
30
- --description "Optional description" \
31
- --tags "shorts,demo" \
32
- --privacy public
33
- ```
34
-
35
- ## Options
36
-
37
- | Option | Required | Description |
38
- |----------------|----------|--------------------------------------------------|
39
- | `--file` | Yes | Path to video file (absolute or relative to script dir). |
40
- | `--title` | Yes | Video title. |
41
- | `--description`| No | Video description (default: empty). |
42
- | `--tags` | No | Comma-separated tags (e.g. `shorts,demo`). |
43
- | `--privacy` | No | `public`, `private`, or `unlisted` (default: public). |
44
-
45
- ## Agent usage (exec tool)
46
-
47
- Use the **exec** tool with `skill_id: "youtube"` so the command runs in the YouTube skill directory:
48
-
49
- ```json
50
- {
51
- "skill_id": "youtube",
52
- "command": "python3 scripts/youtube_upload.py --file /path/to/video.mp4 --title \"My title\" --description \"Description\" --tags \"tag1,tag2\" --privacy public"
53
- }
54
- ```
55
-
56
- Ensure the video file path is accessible from the environment where the agent runs (e.g. workspace path or absolute path). Quotes inside the command string must be escaped as required by the shell.
57
-
58
- ## Troubleshooting
59
-
60
- | Problem | Cause | Action |
61
- |----------------------------|---------------------------------|---------------------------------------------|
62
- | client_secret.json not found / YOUTUBE_CLIENT_SECRET_JSON not set | Credentials missing | Add via Skills → YouTube → Configure or place `scripts/client_secret.json`. |
63
- | Token expired / invalid | First run or token revoked | Delete `token.json` and run again to re-auth. |
64
- | File not found | Wrong path for `--file` | Use absolute path or path relative to script dir. |
65
- | Quota exceeded | YouTube API quota | Check quota in Cloud Console; wait or request increase. |
@@ -1,3 +0,0 @@
1
- google-api-python-client
2
- google-auth-oauthlib
3
- google-auth-httplib2
@@ -1,200 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Upload a video to YouTube. Uses OAuth credentials from YOUTUBE_CLIENT_SECRET_JSON
4
- * (Skills config) or scripts/client_secret.json. Saves token.json in scripts/ for reuse.
5
- *
6
- * Usage: node scripts/youtube_upload.js --file VIDEO.mp4 --title "Title" [--description "..." ] [--tags "a,b"] [--privacy public|private|unlisted]
7
- *
8
- * Dependencies: npm install (in the skill directory). Agent can run: exec with skill_id "youtube", command "npm install".
9
- */
10
-
11
- const fs = require("fs");
12
- const path = require("path");
13
- const http = require("http");
14
-
15
- const SCRIPT_DIR = path.resolve(path.dirname(__filename));
16
- const SKILL_DIR = path.resolve(SCRIPT_DIR, "..");
17
- const TOKEN_PATH = path.join(SCRIPT_DIR, "token.json");
18
- const SCOPES = ["https://www.googleapis.com/auth/youtube.upload"];
19
- const REDIRECT_URI = "http://localhost:8090/";
20
-
21
- function parseArgs() {
22
- const args = process.argv.slice(2);
23
- const out = { file: null, title: null, description: "", tags: "", privacy: "public" };
24
- for (let i = 0; i < args.length; i++) {
25
- if (args[i] === "--file" && args[i + 1]) out.file = args[++i];
26
- else if (args[i] === "--title" && args[i + 1]) out.title = args[++i];
27
- else if (args[i] === "--description" && args[i + 1]) out.description = args[++i];
28
- else if (args[i] === "--tags" && args[i + 1]) out.tags = args[++i];
29
- else if (args[i] === "--privacy" && args[i + 1]) out.privacy = args[++i];
30
- }
31
- return out;
32
- }
33
-
34
- function getClientSecret() {
35
- const fromEnv = process.env.YOUTUBE_CLIENT_SECRET_JSON;
36
- if (fromEnv && fromEnv.trim()) {
37
- try {
38
- return JSON.parse(fromEnv.trim());
39
- } catch (e) {
40
- console.error("ERROR: YOUTUBE_CLIENT_SECRET_JSON is not valid JSON.", e.message);
41
- process.exit(1);
42
- }
43
- }
44
- const p = path.join(SCRIPT_DIR, "client_secret.json");
45
- if (!fs.existsSync(p)) {
46
- console.error("ERROR: client_secret.json not found and YOUTUBE_CLIENT_SECRET_JSON not set.");
47
- console.error("Add via Skills → YouTube → Configure or place scripts/client_secret.json.");
48
- process.exit(1);
49
- }
50
- return JSON.parse(fs.readFileSync(p, "utf8"));
51
- }
52
-
53
- function getOAuth2Client(credentials) {
54
- const { google } = require("googleapis");
55
- const client = credentials.installed || credentials.web;
56
- if (!client) {
57
- console.error("ERROR: client_secret JSON must have 'installed' or 'web' with client_id and client_secret.");
58
- process.exit(1);
59
- }
60
- return new google.auth.OAuth2(client.client_id, client.client_secret, REDIRECT_URI);
61
- }
62
-
63
- function loadSavedToken() {
64
- if (!fs.existsSync(TOKEN_PATH)) return null;
65
- try {
66
- return JSON.parse(fs.readFileSync(TOKEN_PATH, "utf8"));
67
- } catch {
68
- return null;
69
- }
70
- }
71
-
72
- function saveToken(tokens) {
73
- fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2), "utf8");
74
- }
75
-
76
- function authorize(oauth2Client) {
77
- return new Promise((resolve, reject) => {
78
- const authUrl = oauth2Client.generateAuthUrl({
79
- access_type: "offline",
80
- scope: SCOPES,
81
- prompt: "consent",
82
- });
83
- const server = http
84
- .createServer(async (req, res) => {
85
- const url = new URL(req.url || "", `http://localhost`);
86
- const code = url.searchParams.get("code");
87
- res.writeHead(200, { "Content-Type": "text/plain" });
88
- if (code) {
89
- res.end("Authorized. You can close this tab.");
90
- server.close();
91
- try {
92
- const { tokens } = await oauth2Client.getToken(code);
93
- oauth2Client.setCredentials(tokens);
94
- saveToken(tokens);
95
- resolve(oauth2Client);
96
- } catch (e) {
97
- reject(e);
98
- }
99
- } else {
100
- res.end("Missing code. Try again.");
101
- }
102
- })
103
- .listen(8090, "localhost", () => {
104
- console.error("Open this URL in your browser to authorize:");
105
- console.error(authUrl);
106
- const op = require("child_process").spawn(
107
- process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open",
108
- [authUrl],
109
- { stdio: "ignore" }
110
- );
111
- op.on("error", () => {});
112
- });
113
- server.on("error", reject);
114
- });
115
- }
116
-
117
- async function getAuthenticatedClient() {
118
- let credentials;
119
- try {
120
- credentials = getClientSecret();
121
- } catch (e) {
122
- console.error("ERROR:", e.message);
123
- process.exit(1);
124
- }
125
- const oauth2Client = getOAuth2Client(credentials);
126
- const token = loadSavedToken();
127
- if (token) {
128
- oauth2Client.setCredentials(token);
129
- oauth2Client.on("tokens", (tokens) => {
130
- const creds = oauth2Client.credentials;
131
- if (tokens.refresh_token) creds.refresh_token = tokens.refresh_token;
132
- saveToken(creds);
133
- });
134
- return oauth2Client;
135
- }
136
- return authorize(oauth2Client);
137
- }
138
-
139
- async function uploadVideo(oauth2Client, filePath, title, description, tagsList, privacy) {
140
- const { google } = require("googleapis");
141
- const youtube = google.youtube({ version: "v3", auth: oauth2Client });
142
- const body = {
143
- snippet: {
144
- title,
145
- description: description || "",
146
- tags: tagsList,
147
- categoryId: "22",
148
- },
149
- status: { privacyStatus: privacy },
150
- };
151
- const media = {
152
- body: fs.createReadStream(filePath),
153
- };
154
- const res = await youtube.videos.insert({
155
- part: "snippet,status",
156
- requestBody: body,
157
- media,
158
- });
159
- const videoId = res.data.id;
160
- console.log("Uploaded: https://youtube.com/watch?v=" + videoId);
161
- return videoId;
162
- }
163
-
164
- async function main() {
165
- const args = parseArgs();
166
- if (!args.file || !args.title) {
167
- console.error("Usage: node youtube_upload.js --file <path> --title \"Title\" [--description \"...\"] [--tags \"a,b\"] [--privacy public|private|unlisted]");
168
- process.exit(1);
169
- }
170
- if (!["public", "private", "unlisted"].includes(args.privacy)) {
171
- console.error("ERROR: --privacy must be public, private, or unlisted");
172
- process.exit(1);
173
- }
174
- let filePath = path.resolve(args.file);
175
- if (!path.isAbsolute(args.file)) filePath = path.resolve(SCRIPT_DIR, args.file);
176
- if (!fs.existsSync(filePath)) {
177
- console.error("ERROR: File not found:", filePath);
178
- process.exit(1);
179
- }
180
- const tagsList = args.tags ? args.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
181
-
182
- try {
183
- const auth = await getAuthenticatedClient();
184
- await uploadVideo(auth, filePath, args.title, args.description, tagsList, args.privacy);
185
- const creds = auth.credentials;
186
- if (creds && (creds.access_token || creds.refresh_token)) saveToken(creds);
187
- process.exit(0);
188
- } catch (e) {
189
- if (e.code === "MODULE_NOT_FOUND" && (e.message || "").includes("googleapis")) {
190
- console.error("ERROR: Missing npm dependencies. Install with:");
191
- console.error(" npm install");
192
- console.error("Run that from the skill directory (exec with skill_id 'youtube', command 'npm install'), then retry.");
193
- process.exit(1);
194
- }
195
- console.error("ERROR:", e.message || e);
196
- process.exit(1);
197
- }
198
- }
199
-
200
- main();
@@ -1,125 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Upload a video to YouTube using OAuth credentials in the skill directory.
3
-
4
- Usage:
5
- python3 youtube_upload.py --file VIDEO.mp4 --title "Title" --description "Description" [--tags "a,b,c"] [--privacy public|private|unlisted]
6
-
7
- First run: place client_secret.json (from Google Cloud Console) in this script's directory.
8
- Browser OAuth will run once; token.json is saved for reuse.
9
-
10
- Requires: pip install google-api-python-client google-auth-oauthlib google-auth-httplib2
11
- """
12
-
13
- import argparse
14
- import os
15
- import sys
16
- import tempfile
17
- from pathlib import Path
18
-
19
- SCRIPT_DIR = Path(__file__).resolve().parent
20
- SKILL_DIR = SCRIPT_DIR.parent
21
- TOKEN_FILE = SCRIPT_DIR / "token.json"
22
- SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
23
-
24
- REQUIREMENTS_MSG = (
25
- "YouTube skill requires Google API packages. Install them with:\n"
26
- f" pip install -r \"{SKILL_DIR / 'requirements.txt'}\"\n"
27
- "Or: pip install google-api-python-client google-auth-oauthlib google-auth-httplib2"
28
- )
29
-
30
-
31
- def _client_secret_path() -> Path:
32
- """Path to client secrets: from env YOUTUBE_CLIENT_SECRET_JSON (Skills UI) or scripts/client_secret.json."""
33
- raw = os.environ.get("YOUTUBE_CLIENT_SECRET_JSON", "").strip()
34
- if raw:
35
- try:
36
- f = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
37
- f.write(raw)
38
- f.close()
39
- return Path(f.name)
40
- except Exception:
41
- pass
42
- return SCRIPT_DIR / "client_secret.json"
43
-
44
-
45
- def get_authenticated_service():
46
- try:
47
- from google.auth.transport.requests import Request
48
- from google.oauth2.credentials import Credentials
49
- from google_auth_oauthlib.flow import InstalledAppFlow
50
- from googleapiclient.discovery import build
51
- except ModuleNotFoundError:
52
- print("ERROR: Missing required Google API packages.", file=sys.stderr)
53
- print(REQUIREMENTS_MSG, file=sys.stderr)
54
- sys.exit(1)
55
-
56
- client_secret_file = _client_secret_path()
57
- creds = None
58
- if TOKEN_FILE.exists():
59
- creds = Credentials.from_authorized_user_file(str(TOKEN_FILE), SCOPES)
60
- if not creds or not creds.valid:
61
- if creds and creds.expired and creds.refresh_token:
62
- creds.refresh(Request())
63
- else:
64
- if not client_secret_file.exists():
65
- print("ERROR: client_secret.json not found and YOUTUBE_CLIENT_SECRET_JSON not set.", file=sys.stderr)
66
- print("Add via Skills → YouTube → Configure (paste OAuth client JSON) or place scripts/client_secret.json.", file=sys.stderr)
67
- sys.exit(1)
68
- flow = InstalledAppFlow.from_client_secrets_file(str(client_secret_file), SCOPES)
69
- creds = flow.run_local_server(port=8090, open_browser=True)
70
- if client_secret_file != SCRIPT_DIR / "client_secret.json":
71
- try:
72
- client_secret_file.unlink()
73
- except Exception:
74
- pass
75
- TOKEN_FILE.write_text(creds.to_json())
76
- return build("youtube", "v3", credentials=creds)
77
-
78
-
79
- def upload(youtube, file_path: str, title: str, description: str, tags: list[str], privacy: str, category_id: str = "22"):
80
- from googleapiclient.http import MediaFileUpload
81
-
82
- body = {
83
- "snippet": {
84
- "title": title,
85
- "description": description,
86
- "tags": tags,
87
- "categoryId": category_id,
88
- },
89
- "status": {"privacyStatus": privacy},
90
- }
91
- media = MediaFileUpload(file_path, chunksize=1024 * 1024, resumable=True)
92
- request = youtube.videos().insert(part="snippet,status", body=body, media_body=media)
93
- response = None
94
- while response is None:
95
- status, response = request.next_chunk()
96
- if status:
97
- print(f" Progress: {int(status.progress() * 100)}%")
98
- video_id = response["id"]
99
- print(f"Uploaded: https://youtube.com/watch?v={video_id}")
100
- return video_id
101
-
102
-
103
- def main():
104
- ap = argparse.ArgumentParser(description="Upload a video to YouTube.")
105
- ap.add_argument("--file", required=True, help="Path to video file (e.g. video.mp4)")
106
- ap.add_argument("--title", required=True, help="Video title")
107
- ap.add_argument("--description", default="", help="Video description")
108
- ap.add_argument("--tags", default="", help="Comma-separated tags (e.g. shorts,demo)")
109
- ap.add_argument("--privacy", default="public", choices=("public", "private", "unlisted"), help="Privacy status")
110
- args = ap.parse_args()
111
-
112
- file_path = Path(args.file)
113
- if not file_path.is_absolute():
114
- file_path = SCRIPT_DIR / file_path
115
- if not file_path.exists():
116
- print(f"ERROR: File not found: {file_path}", file=sys.stderr)
117
- sys.exit(1)
118
-
119
- tags_list = [t.strip() for t in args.tags.split(",") if t.strip()]
120
- youtube = get_authenticated_service()
121
- upload(youtube, str(file_path), args.title, args.description, tags_list, args.privacy)
122
-
123
-
124
- if __name__ == "__main__":
125
- main()