Files
magnus919_agent-skills/peertube/scripts/peertube-cli
T

240 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
"""peertube-cli — PeerTube federated video from the terminal.
Browse videos, channels, and playlists on any PeerTube instance.
Login with OAuth2 for authenticated operations.
"""
import argparse, json, os, sys, time, warnings
from typing import Any, Dict, List, Optional, Tuple
warnings.simplefilter("ignore")
import requests
ENV_SERVER = os.getenv("PEERTUBE_SERVER", "")
CONFIG_DIR = os.path.expanduser(os.getenv("PEERTUBE_CONFIG_DIR", "~/.config/peertube-cli"))
API_BASE = "/api/v1"
QUIET = False
GLOBAL_FLAGS: Dict[str, Any] = {"json": False, "dry_run": False, "quiet": False, "verbose": False}
def log(m): global QUIET; (not QUIET and not GLOBAL_FLAGS.get("json")) and print(m)
def warn(m): print(f"Warning: {m}", file=sys.stderr)
def die(m, c=1): print(f"Error: {m}", file=sys.stderr); sys.exit(c)
def emit(h, d):
if GLOBAL_FLAGS.get("json"): print(json.dumps(d, default=str))
else: print(h)
def _preparse(argv):
BOOLS = {"--json","--dry-run","--quiet","--verbose"}
f, fl = {}, [argv[0]]
i = 1
while i < len(argv):
a = argv[i]
if a in BOOLS: f[a.lstrip("-").replace("-","_")] = True; i += 1
elif a in ("--help","-h"): return f, argv
elif a == "--": fl.extend(argv[i:]); break
else: fl.append(a); i += 1
return f, fl
class PeerTubeClient:
def __init__(self, server="", dry_run=False):
self.server = (server or ENV_SERVER or "https://your-instance.example.com").rstrip("/")
self.dry_run = dry_run
self._token = None
# Try to load saved token
token_path = os.path.join(CONFIG_DIR, "token.json")
if os.path.isfile(token_path):
try:
with open(token_path) as f: data = json.load(f)
if data.get("expires_at", 0) > time.time():
self._token = data.get("access_token")
except: pass
def _oauth_login(self):
"""Fetch OAuth client creds and exchange for token."""
try:
r = requests.get(f"{self.server}{API_BASE}/oauth-clients/local", timeout=15)
clients = r.json()
except: die(f"Cannot reach {self.server}. Check PEERTUBE_SERVER.")
return clients.get("client_id", ""), clients.get("client_secret", "")
def login(self, username, password):
"""Login and persist OAuth token."""
cid, csec = self._oauth_login()
try:
r = requests.post(f"{self.server}{API_BASE}/users/token", data={
"client_id": cid, "client_secret": csec,
"grant_type": "password", "username": username,
"password": password,
"response_type": "code"}, timeout=15)
except ConnectionError as e: die(f"Cannot connect: {e}")
if r.status_code >= 400: die(f"Login failed: {r.text[:200]}")
data = r.json()
self._token = data.get("access_token")
# Persist token
os.makedirs(CONFIG_DIR, exist_ok=True)
with open(os.path.join(CONFIG_DIR, "token.json"), "w") as f:
json.dump({"access_token": self._token, "expires_at": time.time() + data.get("expires_in", 86400)}, f)
return data
def _headers(self):
h = {"Accept": "application/json"}
if self._token: h["Authorization"] = f"Bearer {self._token}"
return h
def _get(self, path, params=None):
url = f"{self.server}{API_BASE}{path}"
if self.dry_run: return {"dry_run":True, "url":url, "params":params, "total":0, "data":[]}
try:
r = requests.get(url, params=params, headers=self._headers(), timeout=30)
except ConnectionError as e: die(f"Cannot connect: {e}")
if r.status_code == 401:
die("Not authenticated. Run 'peertube auth login' first.")
if r.status_code >= 400:
try: d = r.json()
except: d = r.text[:200]
die(f"API error ({r.status_code}): {d}")
return r.json()
def get_server_info(self): return self._get("/server/")
def list_videos(self, page=1, limit=12, sort="-publishedAt"):
return self._get("/videos", {"page":page, "count":limit, "sort":sort})
def search_videos(self, query, page=1, limit=12):
return self._get("/search/videos", {"search":query, "page":page, "count":limit})
def get_video(self, vid): return self._get(f"/videos/{vid}")
def list_video_comments(self, vid, page=1, limit=10):
return self._get(f"/videos/{vid}/comments", {"page":page, "count":limit})
def list_channels(self, page=1, limit=15):
return self._get("/video-channels", {"page":page, "count":limit})
def get_channel(self, name): return self._get(f"/video-channels/{name}")
def list_channel_videos(self, name, page=1, limit=12):
return self._get(f"/video-channels/{name}/videos", {"page":page, "count":limit})
def list_playlists(self, page=1, limit=10):
return self._get("/video-playlists/user", {"page":page, "count":limit})
def get_playlist(self, pid): return self._get(f"/video-playlists/{pid}")
def my_profile(self): return self._get("/users/me")
def my_videos(self, page=1, limit=12): return self._get("/users/me/videos", {"page":page, "count":limit})
def fmt_video(v, idx=None):
prefix = f"{idx}. " if idx else ""
name = v.get("name", "?")
author = v.get("channel",{}).get("displayName", v.get("account",{}).get("displayName","?"))
dur = v.get("duration", 0)
dur_str = f"{int(dur//60)}:{int(dur%60):02d}"
views = v.get("views", 0)
pub = (v.get("publishedAt") or "")[:10]
return f" {prefix}{name:55} {dur_str} {views} views {author} {pub}"
def cmd_server(client, args):
if client.dry_run: return emit("[dry-run] Get server info", {"dry_run":True})
d = client.get_server_info() or {}
name = d.get("instance",{}).get("name","?")
desc = (d.get("instance",{}).get("shortDescription","") or "")[:80]
users = d.get("users","?"); videos = d.get("videos","?"); views = d.get("views","?")
emit(f"🖥️ {name}\n {desc}\n Users: {users} Videos: {videos} Views: {views}",
{"instance":{"name":name,"shortDescription":desc},"users":users,"videos":videos})
def cmd_videos(client, args):
p = argparse.ArgumentParser(prog="peertube videos")
p.add_argument("--limit", type=int, default=12)
parsed, _ = p.parse_known_args(args)
if client.dry_run: return emit("[dry-run] List videos", {"dry_run":True})
data = client.list_videos(limit=parsed.limit) or {}
videos = data.get("data",[])
if not videos: return emit("No videos.", {"videos":[]})
lines = [fmt_video(v, i+1) for i, v in enumerate(videos)]
total = data.get("total", len(videos))
emit(f"{total} video(s):\n"+"\n".join(lines), {"total":total,"videos":videos})
def cmd_search(client, args):
p = argparse.ArgumentParser(prog="peertube search")
p.add_argument("--query", "-q", required=True)
p.add_argument("--limit", type=int, default=12)
parsed, _ = p.parse_known_args(args)
if client.dry_run: return emit(f"[dry-run] Search: {parsed.query}", {"dry_run":True})
data = client.search_videos(parsed.query, limit=parsed.limit) or {}
videos = data.get("data",[])
if not videos: return emit("No results.", {"videos":[]})
lines = [fmt_video(v, i+1) for i, v in enumerate(videos)]
total = data.get("total", len(videos))
emit(f"{total} result(s):\n"+"\n".join(lines), {"total":total,"videos":videos})
def cmd_channels(client, args):
if client.dry_run: return emit("[dry-run] List channels", {"dry_run":True})
data = client.list_channels() or {}
channels = data.get("data",[])
if not channels: return emit("No channels.", {"channels":[]})
lines, out = [], []
for c in channels:
dn = c.get("displayName","?"); name = c.get("name","?"); vid = c.get("videosCount",0); subs = c.get("subscribersCount",0)
lines.append(f" {dn:30} @{name} {vid} videos {subs} subscribers")
out.append({"displayName":dn,"name":name,"videosCount":vid,"subscribersCount":subs})
emit(f"{len(channels)} channel(s):\n"+"\n".join(lines), {"channels":out})
def cmd_me(client, args):
if client.dry_run: return emit("[dry-run] Get profile", {"dry_run":True})
d = client.my_profile() or {}
uname = d.get("username","?"); role = d.get("role","?")
vid = d.get("videosCount",0); views = d.get("viewsCount",0)
emit(f"👤 @{uname} Role: {role} Videos: {vid} Views: {views}",
{"username":uname,"role":role,"videosCount":vid,"viewsCount":views})
def main():
global GLOBAL_FLAGS, QUIET
GLOBAL_FLAGS, filtered_argv = _preparse(sys.argv)
if GLOBAL_FLAGS.get("quiet"): QUIET = True
if GLOBAL_FLAGS.get("json"): warnings.simplefilter("ignore")
parser = argparse.ArgumentParser(prog="peertube", description="PeerTube federated video.",
epilog="Set PEERTUBE_SERVER. Login: peertube auth login --username ... --password ...")
sub = parser.add_subparsers(dest="command")
# auth
ap = sub.add_parser("auth", help="Authentication")
asub = ap.add_subparsers(dest="auth_action")
lp = asub.add_parser("login", help="Login to PeerTube instance")
lp.add_argument("--username", required=True); lp.add_argument("--password", required=True)
# other commands
sub.add_parser("server", help="Server info")
sub.add_parser("videos", help="List videos").add_argument("--limit",type=int,default=12)
sub.add_parser("channels", help="List channels")
sp = sub.add_parser("search", help="Search videos")
sp.add_argument("--query","-q",required=True); sp.add_argument("--limit",type=int,default=12)
sub.add_parser("me", help="My profile")
args = parser.parse_args(filtered_argv[1:])
if not args.command: parser.print_help(); sys.exit(1)
client = PeerTubeClient(dry_run=GLOBAL_FLAGS.get("dry_run",False))
remaining = filtered_argv[filtered_argv.index(args.command)+1:]
if args.command == "auth":
if args.auth_action == "login":
result = client.login(args.username, args.password)
emit(f"✅ Logged in to {client.server}", {"status":"logged_in","server":client.server})
else: ap.print_help()
return
# Commands that need auth
if not client._token and not GLOBAL_FLAGS.get("dry_run"):
die("Not logged in. Run 'peertube auth login --username ... --password ...' first, or use --dry-run.")
handlers = {"server":cmd_server,"videos":cmd_videos,"search":cmd_search,"channels":cmd_channels,"me":cmd_me}
h = handlers.get(args.command)
if not h: parser.print_help(); sys.exit(1)
h(client, remaining)
if __name__ == "__main__": main()