Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ command line.
- Comments moderation: list, reply, and moderate from the CLI.
- Channel analytics queries via the YouTube Analytics API.

## Try in 10 seconds

No account, no OAuth, no API setup. Runs against a built-in fake channel:

```bash
pipx install ytstudio-cli
ytstudio demo tour
```

Use `ytstudio init && ytstudio login` to connect your real channel.

## Documentation

See the [full documentation](https://jdwit.github.io/ytstudio-cli/) for installation, OAuth setup, and the command reference.
Expand Down
11 changes: 11 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
Manage and analyze your YouTube channel from the terminal. Designed for
humans and AI agents.

## Try in 10 seconds

No account, no OAuth, no API setup. Runs against a built-in fake channel:

```bash
pipx install ytstudio-cli
ytstudio demo tour
```

Use `ytstudio init && ytstudio login` to connect your real channel.

## What it is

A small CLI on top of the official
Expand Down
20 changes: 20 additions & 0 deletions scripts/terminaltrove-demo.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# terminaltrove demo tape for ytstudio-cli
#
# Records the first-run demo mode end-to-end against the built-in fake channel.
# No OAuth, no network, no sandbox account required.
#
# Render with: vhs scripts/terminaltrove-demo.tape

Output scripts/terminaltrove-demo.gif

Set FontSize 16
Set Width 1200
Set Height 720
Set Padding 20
Set Theme "Dracula"
Set Shell "bash"

Type "ytstudio demo tour --no-pauses"
Sleep 500ms
Enter
Sleep 4s
14 changes: 14 additions & 0 deletions src/ytstudio/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,20 @@ def get_authenticated_service(


def get_status(profile: str | None = None) -> None:
# Local import avoids importing demo fixtures in the hot real-auth path.
from ytstudio.demo_service import _load_fixture, is_demo_mode # noqa: PLC0415

if is_demo_mode():
channel = _load_fixture("channel.json")["items"][0]
snippet = channel["snippet"]
stats = channel["statistics"]
success_message("Authenticated (demo mode)")
console.print(" mode: demo")
console.print(f" Channel: [bold]{snippet['title']}[/bold]")
console.print(f" Subscribers: {stats.get('subscriberCount', 'N/A')}")
console.print(f" Videos: {stats.get('videoCount', 'N/A')}")
return

creds_data = load_credentials(profile)

if not creds_data:
Expand Down
131 changes: 131 additions & 0 deletions src/ytstudio/commands/demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""First-run demo mode: try the CLI against a built-in fake channel.

Each subcommand sets `YTSTUDIO_DEMO=1` for the duration of the call and then
delegates to the real command function, so users see identical output to the
production code path without needing an account or OAuth credentials.
"""

import os
import time
from contextlib import contextmanager

import typer

from ytstudio.commands import analytics as analytics_cmd
from ytstudio.commands import comments as comments_cmd
from ytstudio.commands import videos as videos_cmd
from ytstudio.ui import console, create_kv_table, dim

app = typer.Typer(help="Try the CLI against a built-in fake channel - no OAuth required")


@contextmanager
def _demo_env():
previous = os.environ.get("YTSTUDIO_DEMO")
os.environ["YTSTUDIO_DEMO"] = "1"
try:
yield
finally:
if previous is None:
os.environ.pop("YTSTUDIO_DEMO", None)
else:
os.environ["YTSTUDIO_DEMO"] = previous


@app.command()
def videos(
limit: int = typer.Option(5, "--limit", "-n", help="Number of videos to list"),
output: str = typer.Option("table", "--output", "-o", help="Output format: table, json"),
):
"""List fake videos from the demo channel."""
with _demo_env():
videos_cmd.list_videos(
limit=limit,
page_token=None,
sort="date",
output=output,
audio_lang=None,
meta_lang=None,
has_localization=None,
scheduled=False,
)


@app.command()
def analytics(
days: int = typer.Option(28, "--days", "-d", help="Number of days to analyze"),
output: str = typer.Option("table", "--output", "-o", help="Output format: table, json"),
):
"""Show fake channel analytics overview."""
with _demo_env():
analytics_cmd.overview(days=days, output=output)


@app.command()
def comments(
limit: int = typer.Option(10, "--limit", "-n", help="Number of comments"),
output: str = typer.Option("table", "--output", "-o", help="Output format: table, json"),
):
"""List fake comments across the demo channel."""
with _demo_env():
comments_cmd.list_comments(
video_id=None,
status=comments_cmd.ModerationStatus.published,
limit=limit,
sort=comments_cmd.SortOrder.time,
output=output,
)


@app.command()
def tour(
no_pauses: bool = typer.Option(False, "--no-pauses", help="Skip the pacing sleeps"),
):
"""Run videos, analytics, and comments back-to-back."""
console.print(dim("\nytstudio demo tour: videos -> analytics -> comments\n"))

with _demo_env():
console.print(dim("[1/3] videos\n"))
videos_cmd.list_videos(
limit=5,
page_token=None,
sort="date",
output="table",
audio_lang=None,
meta_lang=None,
has_localization=None,
scheduled=False,
)
if not no_pauses:
time.sleep(1.5)

console.print(dim("\n[2/3] analytics\n"))
analytics_cmd.overview(days=28, output="table")
if not no_pauses:
time.sleep(1.5)

console.print(dim("\n[3/3] comments\n"))
comments_cmd.list_comments(
video_id=None,
status=comments_cmd.ModerationStatus.published,
limit=10,
sort=comments_cmd.SortOrder.time,
output="table",
)

console.print(dim("\nDone. Connect your real channel with: ytstudio init && ytstudio login\n"))


@app.command()
def info():
"""Show what powers demo mode."""
fixtures_path = "ytstudio/fixtures/ (bundled in the wheel)"
table = create_kv_table()
table.add_column("field", style="dim")
table.add_column("value")
table.add_row("fixtures", fixtures_path)
table.add_row("env var", "YTSTUDIO_DEMO")
table.add_row("channel", "Demo Creator Channel (@democreator)")
table.add_row("network", "none; quota/error paths are bypassed")
console.print(table)
console.print("\n[dim]To use your real channel: ytstudio init && ytstudio login[/dim]")
Loading