Spaces:
Paused
Paused
| #!/usr/bin/env python3 | |
| """YouTube CLI tool for Hermes Agent - uses OAuth2 refresh token flow.""" | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import mimetypes | |
| from urllib.parse import urlencode | |
| try: | |
| import requests | |
| except ImportError: | |
| import subprocess | |
| subprocess.check_call([sys.executable, "-m", "pip", "install", "requests", "-q"]) | |
| import requests | |
| def get_access_token(): | |
| """Get a fresh access token using the refresh token.""" | |
| client_id = os.environ.get("YOUTUBE_CLIENT_ID") | |
| client_secret = os.environ.get("YOUTUBE_CLIENT_SECRET") | |
| refresh_token = os.environ.get("YOUTUBE_REFRESH_TOKEN") | |
| if not all([client_id, client_secret, refresh_token]): | |
| print("ERROR: Missing YouTube credentials. Set YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET, YOUTUBE_REFRESH_TOKEN") | |
| sys.exit(1) | |
| r = requests.post("https://oauth2.googleapis.com/token", data={ | |
| "client_id": client_id, | |
| "client_secret": client_secret, | |
| "refresh_token": refresh_token, | |
| "grant_type": "refresh_token", | |
| }) | |
| if r.status_code != 200: | |
| print(f"ERROR: Token refresh failed: {r.text}") | |
| sys.exit(1) | |
| return r.json()["access_token"] | |
| def api(method, endpoint, token, params=None, json_data=None, files=None): | |
| """Make a YouTube API request.""" | |
| url = f"https://www.googleapis.com/youtube/v3/{endpoint}" | |
| headers = {"Authorization": f"Bearer {token}"} | |
| if method == "GET": | |
| r = requests.get(url, headers=headers, params=params, timeout=30) | |
| elif method == "POST": | |
| if files: | |
| r = requests.post(url, headers=headers, params=params, files=files, timeout=600) | |
| else: | |
| headers["Content-Type"] = "application/json" | |
| r = requests.post(url, headers=headers, params=params, json=json_data, timeout=30) | |
| elif method == "PUT": | |
| headers["Content-Type"] = "application/json" | |
| r = requests.put(url, headers=headers, params=params, json=json_data, timeout=30) | |
| elif method == "DELETE": | |
| r = requests.delete(url, headers=headers, params=params, timeout=30) | |
| return r | |
| def cmd_channel(args): | |
| """Show channel info.""" | |
| token = get_access_token() | |
| r = api("GET", "channels", token, params={"part": "snippet,statistics,contentDetails", "mine": "true"}) | |
| data = r.json() | |
| if not data.get("items"): | |
| print("No channel found.") | |
| return | |
| ch = data["items"][0] | |
| s = ch["snippet"] | |
| stats = ch["statistics"] | |
| print(f"Channel: {s['title']}") | |
| print(f"Description: {s.get('description', 'N/A')}") | |
| print(f"Subscribers: {stats.get('subscriberCount', 'hidden')}") | |
| print(f"Videos: {stats.get('videoCount', '0')}") | |
| print(f"Views: {stats.get('viewCount', '0')}") | |
| print(f"Created: {s.get('publishedAt', 'N/A')}") | |
| print(f"Channel ID: {ch['id']}") | |
| def cmd_list(args): | |
| """List recent videos.""" | |
| token = get_access_token() | |
| # Get uploads playlist | |
| r = api("GET", "channels", token, params={"part": "contentDetails", "mine": "true"}) | |
| data = r.json() | |
| if not data.get("items"): | |
| print("No channel found.") | |
| return | |
| uploads_id = data["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"] | |
| # Get videos from uploads playlist | |
| params = {"part": "snippet,status", "playlistId": uploads_id, "maxResults": args.count} | |
| r = api("GET", "playlistItems", token, params=params) | |
| data = r.json() | |
| if not data.get("items"): | |
| print("No videos found.") | |
| return | |
| print(f"{'#':<3} {'Title':<50} {'Status':<12} {'Published':<20}") | |
| print("-" * 90) | |
| for i, item in enumerate(data["items"], 1): | |
| s = item["snippet"] | |
| vid = s["resourceId"]["videoId"] | |
| title = s["title"][:48] | |
| status = item.get("status", {}).get("privacyStatus", "?") | |
| published = s.get("publishedAt", "?")[:19] | |
| print(f"{i:<3} {title:<50} {status:<12} {published}") | |
| print(f" https://youtu.be/{vid}") | |
| def cmd_upload(args): | |
| """Upload a video.""" | |
| if not os.path.exists(args.file): | |
| print(f"ERROR: File not found: {args.file}") | |
| sys.exit(1) | |
| token = get_access_token() | |
| # Step 1: Initialize resumable upload | |
| metadata = { | |
| "snippet": { | |
| "title": args.title, | |
| "description": args.description or "", | |
| "tags": args.tags.split(",") if args.tags else [], | |
| "categoryId": args.category or "22", # 22 = People & Blogs | |
| }, | |
| "status": { | |
| "privacyStatus": args.privacy or "private", | |
| "selfDeclaredMadeForKids": False, | |
| } | |
| } | |
| headers = { | |
| "Authorization": f"Bearer {token}", | |
| "Content-Type": "application/json", | |
| "X-Upload-Content-Type": mimetypes.guess_type(args.file)[0] or "video/*", | |
| "X-Upload-Content-Length": str(os.path.getsize(args.file)), | |
| } | |
| r = requests.post( | |
| "https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status", | |
| headers=headers, | |
| json=metadata, | |
| timeout=30, | |
| ) | |
| if r.status_code != 200: | |
| print(f"ERROR: Upload init failed: {r.status_code} {r.text}") | |
| sys.exit(1) | |
| upload_url = r.headers["Location"] | |
| # Step 2: Upload the file | |
| file_size = os.path.getsize(args.file) | |
| print(f"Uploading: {args.file} ({file_size / 1048576:.1f} MB)") | |
| print(f"Title: {args.title}") | |
| print(f"Privacy: {args.privacy or 'private'}") | |
| with open(args.file, "rb") as f: | |
| r = requests.put( | |
| upload_url, | |
| headers={ | |
| "Authorization": f"Bearer {token}", | |
| "Content-Type": mimetypes.guess_type(args.file)[0] or "video/*", | |
| "Content-Length": str(file_size), | |
| }, | |
| data=f, | |
| timeout=3600, | |
| ) | |
| if r.status_code == 200: | |
| vid = r.json() | |
| print(f"\n✅ Uploaded successfully!") | |
| print(f"Video ID: {vid['id']}") | |
| print(f"URL: https://youtu.be/{vid['id']}") | |
| print(f"Status: {vid['status']['uploadStatus']}") | |
| else: | |
| print(f"ERROR: Upload failed: {r.status_code} {r.text}") | |
| def cmd_search(args): | |
| """Search videos on the channel.""" | |
| token = get_access_token() | |
| # Get channel ID | |
| r = api("GET", "channels", token, params={"part": "id", "mine": "true"}) | |
| channel_id = r.json()["items"][0]["id"] | |
| params = { | |
| "part": "snippet", | |
| "channelId": channel_id, | |
| "q": args.query, | |
| "type": "video", | |
| "maxResults": args.count, | |
| "order": "relevance", | |
| } | |
| r = api("GET", "search", token, params=params) | |
| data = r.json() | |
| if not data.get("items"): | |
| print("No results found.") | |
| return | |
| for i, item in enumerate(data["items"], 1): | |
| s = item["snippet"] | |
| vid = item["id"]["videoId"] | |
| print(f"{i}. {s['title']}") | |
| print(f" https://youtu.be/{vid}") | |
| print(f" {s.get('publishedAt', '')[:10]}") | |
| print() | |
| def cmd_delete(args): | |
| """Delete a video by ID.""" | |
| token = get_access_token() | |
| r = api("DELETE", "videos", token, params={"id": args.video_id}) | |
| if r.status_code == 204: | |
| print(f"✅ Video {args.video_id} deleted.") | |
| else: | |
| print(f"ERROR: {r.status_code} {r.text}") | |
| def main(): | |
| parser = argparse.ArgumentParser(description="YouTube CLI for Hermes Agent") | |
| sub = parser.add_subparsers(dest="command", required=True) | |
| # channel | |
| sub.add_parser("channel", help="Show channel info") | |
| # list | |
| p_list = sub.add_parser("list", help="List recent videos") | |
| p_list.add_argument("-n", "--count", type=int, default=10, help="Number of videos") | |
| # upload | |
| p_upload = sub.add_parser("upload", help="Upload a video") | |
| p_upload.add_argument("file", help="Video file path") | |
| p_upload.add_argument("-t", "--title", required=True, help="Video title") | |
| p_upload.add_argument("-d", "--description", default="", help="Video description") | |
| p_upload.add_argument("--tags", default="", help="Comma-separated tags") | |
| p_upload.add_argument("--category", default="22", help="Category ID (default: 22)") | |
| p_upload.add_argument("--privacy", default="private", choices=["public", "unlisted", "private"]) | |
| # search | |
| p_search = sub.add_parser("search", help="Search channel videos") | |
| p_search.add_argument("query", help="Search query") | |
| p_search.add_argument("-n", "--count", type=int, default=5) | |
| # delete | |
| p_del = sub.add_parser("delete", help="Delete a video") | |
| p_del.add_argument("video_id", help="Video ID to delete") | |
| args = parser.parse_args() | |
| commands = { | |
| "channel": cmd_channel, | |
| "list": cmd_list, | |
| "upload": cmd_upload, | |
| "search": cmd_search, | |
| "delete": cmd_delete, | |
| } | |
| commands[args.command](args) | |
| if __name__ == "__main__": | |
| main() | |