Skip to content

Commit 975c620

Browse files
committed
refactor and imrprove readme
1 parent f06252b commit 975c620

File tree

20 files changed

+1460
-786
lines changed

20 files changed

+1460
-786
lines changed

.gitignore

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,14 @@
1-
# Python
21
__pycache__/
32
*.py[cod]
4-
*$py.class
5-
*.so
6-
.Python
73
.venv/
8-
venv/
94
*.egg-info/
105
dist/
116
build/
12-
.eggs/
13-
14-
# Testing
157
.pytest_cache/
168
.coverage
179
coverage.xml
1810
htmlcov/
19-
20-
# IDE
11+
.ruff_cache/
2112
.idea/
2213
.vscode/
23-
*.swp
24-
*.swo
25-
26-
# OS
2714
.DS_Store
28-
Thumbs.db
29-
30-
# Project specific
31-
.ruff_cache/
32-
uv.lock

README.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33
[![CI](https://github.com/jdwit/ytstudio/actions/workflows/ci.yml/badge.svg)](https://github.com/jdwit/ytstudio/actions/workflows/ci.yml)
44
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
55

6-
Manage your YouTube channel from the terminal. Access analytics and manage videos without leaving the command line. Ideal for agent workflows and automation.
6+
Manage and analyze your YouTube channel from the terminal. Ideal for agent workflows and automation.
7+
8+
## Motivation
9+
10+
I built this tool to bulk update video titles on my channel, something YouTube Studio doesn't support. It uses the YouTube Data API for search-and-replace operations, plus analytics and other channel management features. Simple and scriptable for automating common tasks.
711

812
## Installation
913

14+
I recommend the excellent [uv](https://uv.io/) tool for installation:
15+
1016
```bash
1117
uv tool install ytstudio
1218
```
@@ -15,14 +21,22 @@ uv tool install ytstudio
1521

1622
1. Create a [Google Cloud project](https://console.cloud.google.com/)
1723
2. Enable **YouTube Data API v3** and **YouTube Analytics API**
18-
3. Create OAuth credentials (Desktop app) and download JSON
19-
4. Configure ytstudio:
24+
3. Configure OAuth consent screen:
25+
- Go to **APIs & Services****OAuth consent screen**
26+
- Select **External** and create
27+
- Fill in app name and your email
28+
- Skip scopes, then add yourself as a test user
29+
- Leave the app in "Testing" mode (no verification needed)
30+
4. Create OAuth credentials:
31+
- Go to **APIs & Services****Credentials**
32+
- Click **Create Credentials****OAuth client ID**
33+
- Select **Desktop app** as application type
34+
- Download the JSON file
35+
5. Configure ytstudio:
2036

2137
```bash
22-
ytstudio init --client-secrets path/to/client_secrets.json
38+
ytstudio init --client-secrets path/to/client_secret_<id>.json
2339
ytstudio login
2440
```
2541

26-
## Configuration
27-
2842
Credentials stored in `~/.config/ytstudio/`.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[project]
2-
name = "ytstudio"
2+
name = "ytstudio-cli"
33
version = "0.1.0"
44
description = "CLI tool to manage and analyze your YouTube channel from the terminal"
55
readme = "README.md"
@@ -25,6 +25,7 @@ dependencies = [
2525
"google-auth>=2.0.0",
2626
"google-auth-oauthlib>=1.0.0",
2727
"google-api-python-client>=2.0.0",
28+
"packaging>=21.0",
2829
]
2930

3031
[dependency-groups]

src/ytstudio/auth.py

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,27 @@
1-
"""YouTube OAuth authentication."""
2-
31
from google.auth.transport.requests import Request
42
from google.oauth2.credentials import Credentials
53
from google_auth_oauthlib.flow import InstalledAppFlow
64
from googleapiclient.discovery import build
75
from googleapiclient.errors import HttpError
8-
from rich.console import Console
9-
from rich.panel import Panel
10-
116
from ytstudio.config import (
127
CLIENT_SECRETS_FILE,
138
clear_credentials,
149
load_credentials,
1510
save_credentials,
1611
)
17-
18-
console = Console()
12+
from ytstudio.ui import console, success_message
1913

2014

2115
def handle_api_error(error: HttpError) -> None:
22-
"""Handle YouTube API errors with user-friendly messages."""
2316
if error.resp.status == 403:
2417
error_details = error.error_details[0] if error.error_details else {}
2518
reason = error_details.get("reason", "")
2619

2720
if reason == "quotaExceeded":
2821
console.print(
29-
Panel(
30-
"[bold red]YouTube API quota exceeded[/bold red]\n\n"
31-
"Your daily quota (10,000 units) has been used up.\n\n"
32-
"[bold]Options:[/bold]\n"
33-
"• [cyan]Wait[/cyan] - Quota resets at midnight Pacific Time (~9 AM CET)\n"
34-
"• [cyan]Request more[/cyan] - Visit Google Cloud Console → APIs → YouTube Data API v3 → Quotas\n\n"
35-
"[dim]Tip: Update operations cost 50 units each. Use --dry-run to preview changes.[/dim]",
36-
title="⚠️ Quota Exceeded",
37-
border_style="red",
38-
)
22+
"[red]Daily YouTube API quota exceeded.[/red] "
23+
"Quota resets at midnight Pacific Time (PT).\n"
24+
"See: https://developers.google.com/youtube/v3/guides/quota_and_compliance_audits"
3925
)
4026
raise SystemExit(1)
4127

@@ -67,10 +53,9 @@ def api(request):
6753
]
6854

6955

70-
def authenticate():
71-
"""Run OAuth authentication flow."""
56+
def authenticate() -> None:
7257
if not CLIENT_SECRETS_FILE.exists():
73-
console.print("[red]No client secrets found. Run 'yt init' first.[/red]")
58+
console.print("[red]No client secrets found. Run 'ytstudio init' first.[/red]")
7459
raise SystemExit(1)
7560

7661
console.print("[bold]Authenticating with YouTube...[/bold]\n")
@@ -102,15 +87,15 @@ def authenticate():
10287
service = build("youtube", "v3", credentials=credentials)
10388
response = service.channels().list(part="snippet", mine=True).execute()
10489

90+
console.print()
10591
if response.get("items"):
10692
channel = response["items"][0]["snippet"]
107-
console.print(f"\n[green]✓ Logged in as: {channel['title']}[/green]")
93+
success_message(f"Logged in as: {channel['title']}")
10894
else:
109-
console.print("\n[green]✓ Authentication successful[/green]")
95+
success_message("Authentication successful")
11096

11197

11298
def get_credentials() -> Credentials | None:
113-
"""Get valid credentials, refreshing if needed."""
11499
creds_data = load_credentials()
115100
if not creds_data:
116101
return None
@@ -133,25 +118,26 @@ def get_credentials() -> Credentials | None:
133118
return credentials
134119

135120

136-
def get_authenticated_service(api: str = "youtube", version: str = "v3"):
137-
"""Get an authenticated YouTube API service."""
121+
def get_authenticated_service(api_name: str = "youtube", version: str = "v3"):
122+
import typer
123+
138124
credentials = get_credentials()
139125
if not credentials:
140-
return None
141-
return build(api, version, credentials=credentials)
126+
console.print("[red]Not authenticated. Run 'ytstudio login' first.[/red]")
127+
raise typer.Exit(1)
128+
return build(api_name, version, credentials=credentials)
142129

143130

144-
def get_status():
145-
"""Show authentication status."""
131+
def get_status() -> None:
146132
creds_data = load_credentials()
147133

148134
if not creds_data:
149-
console.print("[yellow]Not authenticated. Run 'yt login' to authenticate.[/yellow]")
135+
console.print("[yellow]Not authenticated. Run 'ytstudio login' to authenticate.[/yellow]")
150136
return
151137

152138
credentials = get_credentials()
153139
if not credentials or not credentials.valid:
154-
console.print("[yellow]Credentials expired. Run 'yt login' to re-authenticate.[/yellow]")
140+
console.print("[yellow]Credentials expired. Run 'ytstudio login' to re-authenticate.[/yellow]")
155141
return
156142

157143
# Get channel info
@@ -163,13 +149,12 @@ def get_status():
163149
snippet = channel["snippet"]
164150
stats = channel["statistics"]
165151

166-
console.print("[green]✓ Authenticated[/green]")
152+
success_message("Authenticated")
167153
console.print(f" Channel: [bold]{snippet['title']}[/bold]")
168154
console.print(f" Subscribers: {stats.get('subscriberCount', 'N/A')}")
169155
console.print(f" Videos: {stats.get('videoCount', 'N/A')}")
170156

171157

172-
def logout():
173-
"""Remove stored credentials."""
158+
def logout() -> None:
174159
clear_credentials()
175-
console.print("[green]✓ Logged out successfully[/green]")
160+
success_message("Logged out successfully")

0 commit comments

Comments
 (0)