mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
240 lines
10 KiB
Python
Executable File
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()
|