feat(skills): Add notion-writer skill and YouTube manager CLI scripts

- Add 02-notion-writer skill with Python script for pushing markdown to Notion
- Add YouTube API CLI scripts for jamie-youtube-manager (channel status, video info, batch update)
- Update jamie-youtube-manager SKILL.md with CLI script documentation
- Update CLAUDE.md with quick reference guides

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-26 19:31:43 +09:00
parent 4d9da597ca
commit c6ab33726f
11 changed files with 2424 additions and 7 deletions

View File

@@ -9,7 +9,15 @@
"Skill(ourdigital-seo-audit)",
"WebFetch(domain:les.josunhotel.com)",
"WebFetch(domain:josunhotel.com)",
"WebFetch(domain:pagespeed.web.dev)"
"WebFetch(domain:pagespeed.web.dev)",
"Bash(ln:*)",
"Skill(skill-creator)",
"Bash(python:*)",
"Bash(mv:*)",
"Bash(ls:*)",
"Bash(cat:*)",
"Bash(git commit:*)",
"Bash(git reset:*)"
]
}
}

View File

@@ -0,0 +1,233 @@
# Notion Writer - Claude Code Skill
> **Purpose**: Push markdown content to Notion pages or databases
> **Platform**: Claude Code (CLI)
> **Input**: Markdown files, Notion URLs
> **Output**: Content written to Notion
---
## Capabilities
| Feature | Input | Output |
|---------|-------|--------|
| Page Content Append | Markdown + Page URL | Appended blocks |
| Page Content Replace | Markdown + Page URL | Replaced content |
| Database Row Create | Markdown + DB URL + Title | New database row |
| Connection Test | API token | Connection status |
| Page/DB Info | URL | Metadata |
---
## Setup
### 1. Create Notion Integration
1. Go to [Notion Integrations](https://www.notion.so/my-integrations)
2. Click "New integration"
3. Name it (e.g., "Claude Writer")
4. Select workspace
5. Copy the "Internal Integration Token"
### 2. Share Pages/Databases with Integration
**Important**: You must share each page or database with your integration:
1. Open the Notion page/database
2. Click "..." menu → "Connections"
3. Add your integration
### 3. Configure Environment
```bash
cd ~/Project/claude-skills-factory/custom-skills/02-notion-writer/code/scripts
# Create .env from example
cp .env.example .env
# Edit and add your token
nano .env
```
`.env` file:
```
NOTION_API_KEY=secret_your_integration_token
```
### 4. Activate Environment
```bash
source venv/bin/activate
```
---
## Usage
### Test Connection
```bash
python notion_writer.py --test
```
### Get Page/Database Info
```bash
# Page info
python notion_writer.py --page "https://notion.so/My-Page-abc123" --info
# Database info
python notion_writer.py --database "https://notion.so/abc123" --info
```
### Write to Page
```bash
# Append content to page
python notion_writer.py --page PAGE_URL --file content.md
# Replace page content
python notion_writer.py --page PAGE_URL --file content.md --replace
# From stdin
cat report.md | python notion_writer.py --page PAGE_URL --stdin
```
### Create Database Row
```bash
# Create row with title and content
python notion_writer.py --database DB_URL --title "New Entry" --file content.md
# Title only
python notion_writer.py --database DB_URL --title "Empty Entry"
```
---
## Markdown Support
### Supported Elements
| Markdown | Notion Block |
|----------|--------------|
| `# Heading` | Heading 1 |
| `## Heading` | Heading 2 |
| `### Heading` | Heading 3 |
| `- item` | Bulleted list |
| `1. item` | Numbered list |
| `- [ ] task` | To-do (unchecked) |
| `- [x] task` | To-do (checked) |
| `> quote` | Quote |
| `` ```code``` `` | Code block |
| `---` | Divider |
| Paragraphs | Paragraph |
### Code Block Languages
Specify language after opening backticks:
```markdown
```python
print("Hello")
```
```
---
## Examples
### Push SEO Audit Report
```bash
python notion_writer.py \
--page "https://notion.so/SEO-Reports-abc123" \
--file ~/reports/seo_audit_2025.md
```
### Create Meeting Notes Entry
```bash
python notion_writer.py \
--database "https://notion.so/Meeting-Notes-abc123" \
--title "Weekly Standup - Dec 26" \
--file meeting_notes.md
```
### Pipe from Another Tool
```bash
# Pipe YouTube video info to Notion
python jamie_video_info.py VIDEO_ID --json | \
python notion_writer.py --page PAGE_URL --stdin
```
---
## API Limits
| Limit | Value |
|-------|-------|
| Blocks per request | 100 |
| Text content per block | 2,000 chars |
| Requests per second | ~3 |
The script automatically batches large content.
---
## Troubleshooting
### "Could not find page"
- Ensure page is shared with your integration
- Check URL/ID is correct
### "Invalid token"
- Verify NOTION_API_KEY in .env
- Token should start with `secret_`
### "Rate limited"
- Wait and retry
- Script handles batching but rapid calls may hit limits
---
## File Structure
```
02-notion-writer/
├── code/
│ ├── CLAUDE.md # This skill document
│ ├── scripts/
│ │ ├── notion_writer.py # Main script
│ │ ├── venv/ # Python environment
│ │ ├── .env # API token (not committed)
│ │ └── .env.example # Template
│ ├── output/ # For generated content
│ └── references/
└── desktop/ # Claude Desktop version (future)
```
---
## Quick Reference
```bash
# Navigate
cd ~/Project/claude-skills-factory/custom-skills/02-notion-writer/code/scripts
source venv/bin/activate
# Test
python notion_writer.py --test
# Write to page
python notion_writer.py -p PAGE_URL -f content.md
# Replace content
python notion_writer.py -p PAGE_URL -f content.md -r
# Create DB row
python notion_writer.py -d DB_URL -t "Title" -f content.md
```
---
*Version 1.0.0 | Claude Code | 2025-12-26*

View File

@@ -0,0 +1,4 @@
# Notion API Configuration
# Get your integration token from: https://www.notion.so/my-integrations
NOTION_API_KEY=secret_your_notion_integration_token_here

View File

@@ -0,0 +1,594 @@
#!/usr/bin/env python3
"""
Notion Writer - Push markdown content to Notion pages or databases.
Supports both page content updates and database row creation.
"""
import os
import sys
import re
import argparse
from pathlib import Path
from typing import Optional, List, Dict, Any
from dotenv import load_dotenv
from notion_client import Client
# Load environment variables
load_dotenv(Path(__file__).parent / '.env')
NOTION_TOKEN = os.getenv('NOTION_API_KEY')
def extract_notion_id(url_or_id: str) -> Optional[str]:
"""Extract Notion page/database ID from URL or raw ID."""
# Already a raw ID (32 chars hex, with or without dashes)
clean_id = url_or_id.replace('-', '')
if re.match(r'^[a-f0-9]{32}$', clean_id):
return clean_id
# Notion URL patterns
patterns = [
r'notion\.so/(?:[^/]+/)?([a-f0-9]{32})', # notion.so/workspace/page-id
r'notion\.so/(?:[^/]+/)?[^-]+-([a-f0-9]{32})', # notion.so/workspace/Page-Title-id
r'notion\.site/(?:[^/]+/)?([a-f0-9]{32})', # public notion.site
r'notion\.site/(?:[^/]+/)?[^-]+-([a-f0-9]{32})',
r'([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})', # UUID format
]
for pattern in patterns:
match = re.search(pattern, url_or_id)
if match:
return match.group(1).replace('-', '')
return None
def format_id_with_dashes(raw_id: str) -> str:
"""Format 32-char ID to UUID format with dashes."""
if len(raw_id) == 32:
return f"{raw_id[:8]}-{raw_id[8:12]}-{raw_id[12:16]}-{raw_id[16:20]}-{raw_id[20:]}"
return raw_id
def markdown_to_notion_blocks(markdown_text: str) -> List[Dict[str, Any]]:
"""Convert markdown text to Notion block objects."""
blocks = []
lines = markdown_text.split('\n')
i = 0
while i < len(lines):
line = lines[i]
# Skip empty lines
if not line.strip():
i += 1
continue
# Headers
if line.startswith('######'):
blocks.append(create_heading_block(line[6:].strip(), 3))
elif line.startswith('#####'):
blocks.append(create_heading_block(line[5:].strip(), 3))
elif line.startswith('####'):
blocks.append(create_heading_block(line[4:].strip(), 3))
elif line.startswith('###'):
blocks.append(create_heading_block(line[3:].strip(), 3))
elif line.startswith('##'):
blocks.append(create_heading_block(line[2:].strip(), 2))
elif line.startswith('#'):
blocks.append(create_heading_block(line[1:].strip(), 1))
# Code blocks
elif line.startswith('```'):
language = line[3:].strip() or 'plain text'
code_lines = []
i += 1
while i < len(lines) and not lines[i].startswith('```'):
code_lines.append(lines[i])
i += 1
blocks.append(create_code_block('\n'.join(code_lines), language))
# Bullet list
elif line.strip().startswith('- ') or line.strip().startswith('* '):
text = line.strip()[2:]
blocks.append(create_bulleted_list_block(text))
# Numbered list
elif re.match(r'^\d+\.\s', line.strip()):
text = re.sub(r'^\d+\.\s', '', line.strip())
blocks.append(create_numbered_list_block(text))
# Checkbox / Todo
elif line.strip().startswith('- [ ]'):
text = line.strip()[5:].strip()
blocks.append(create_todo_block(text, False))
elif line.strip().startswith('- [x]') or line.strip().startswith('- [X]'):
text = line.strip()[5:].strip()
blocks.append(create_todo_block(text, True))
# Blockquote
elif line.startswith('>'):
text = line[1:].strip()
blocks.append(create_quote_block(text))
# Horizontal rule
elif line.strip() in ['---', '***', '___']:
blocks.append(create_divider_block())
# Regular paragraph
else:
blocks.append(create_paragraph_block(line))
i += 1
return blocks
def parse_rich_text(text: str) -> List[Dict[str, Any]]:
"""Parse markdown inline formatting to Notion rich text."""
rich_text = []
# Simple implementation - just plain text for now
# TODO: Add support for bold, italic, code, links
if text:
rich_text.append({
"type": "text",
"text": {"content": text[:2000]} # Notion limit
})
return rich_text
def create_paragraph_block(text: str) -> Dict[str, Any]:
return {
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": parse_rich_text(text)
}
}
def create_heading_block(text: str, level: int) -> Dict[str, Any]:
heading_type = f"heading_{level}"
return {
"object": "block",
"type": heading_type,
heading_type: {
"rich_text": parse_rich_text(text)
}
}
def create_bulleted_list_block(text: str) -> Dict[str, Any]:
return {
"object": "block",
"type": "bulleted_list_item",
"bulleted_list_item": {
"rich_text": parse_rich_text(text)
}
}
def create_numbered_list_block(text: str) -> Dict[str, Any]:
return {
"object": "block",
"type": "numbered_list_item",
"numbered_list_item": {
"rich_text": parse_rich_text(text)
}
}
def create_todo_block(text: str, checked: bool) -> Dict[str, Any]:
return {
"object": "block",
"type": "to_do",
"to_do": {
"rich_text": parse_rich_text(text),
"checked": checked
}
}
def create_quote_block(text: str) -> Dict[str, Any]:
return {
"object": "block",
"type": "quote",
"quote": {
"rich_text": parse_rich_text(text)
}
}
def create_code_block(code: str, language: str) -> Dict[str, Any]:
return {
"object": "block",
"type": "code",
"code": {
"rich_text": parse_rich_text(code),
"language": language.lower()
}
}
def create_divider_block() -> Dict[str, Any]:
return {
"object": "block",
"type": "divider",
"divider": {}
}
def list_accessible_content(notion: Client, filter_type: str = 'all') -> None:
"""List all accessible pages and databases."""
print("=" * 70)
print("Accessible Notion Content (Claude-D.intelligence)")
print("=" * 70)
databases = []
pages = []
try:
# Search all content and filter locally
response = notion.search()
all_results = response.get('results', [])
# Handle pagination
while response.get('has_more'):
response = notion.search(start_cursor=response.get('next_cursor'))
all_results.extend(response.get('results', []))
# Separate databases and pages
for item in all_results:
if item.get('object') == 'database':
databases.append(item)
elif item.get('object') == 'page':
pages.append(item)
# Show databases
if filter_type in ['all', 'database']:
print("\n📊 DATABASES")
print("-" * 70)
if databases:
for i, db in enumerate(databases, 1):
title = ""
if db.get('title'):
title = db['title'][0].get('plain_text', 'Untitled') if db['title'] else 'Untitled'
db_id = db['id']
url = db.get('url', '')
props = list(db.get('properties', {}).keys())[:5]
print(f"\n{i}. {title}")
print(f" ID: {db_id}")
print(f" URL: {url}")
print(f" Properties: {', '.join(props)}")
else:
print(" No databases accessible")
# Show pages
if filter_type in ['all', 'page']:
print("\n\n📄 PAGES")
print("-" * 70)
if pages:
for i, page in enumerate(pages, 1):
title = "Untitled"
if 'properties' in page:
for prop in page['properties'].values():
if prop.get('type') == 'title':
title_arr = prop.get('title', [])
if title_arr:
title = title_arr[0].get('plain_text', 'Untitled')
break
page_id = page['id']
url = page.get('url', '')
parent_type = page.get('parent', {}).get('type', 'unknown')
print(f"\n{i}. {title}")
print(f" ID: {page_id}")
print(f" URL: {url}")
print(f" Parent: {parent_type}")
else:
print(" No pages accessible")
print("\n" + "=" * 70)
db_count = len(databases) if filter_type != 'page' else 0
page_count = len(pages) if filter_type != 'database' else 0
print(f"Total: {db_count} databases, {page_count} pages")
print("=" * 70)
except Exception as e:
print(f"Error listing content: {e}")
def get_page_info(notion: Client, page_id: str) -> Optional[Dict]:
"""Get page information."""
try:
formatted_id = format_id_with_dashes(page_id)
return notion.pages.retrieve(page_id=formatted_id)
except Exception as e:
print(f"Error retrieving page: {e}")
return None
def get_database_info(notion: Client, database_id: str) -> Optional[Dict]:
"""Get database information."""
try:
formatted_id = format_id_with_dashes(database_id)
return notion.databases.retrieve(database_id=formatted_id)
except Exception as e:
print(f"Error retrieving database: {e}")
return None
def append_to_page(notion: Client, page_id: str, blocks: List[Dict]) -> bool:
"""Append blocks to an existing Notion page."""
try:
formatted_id = format_id_with_dashes(page_id)
# Notion API limits to 100 blocks per request
for i in range(0, len(blocks), 100):
batch = blocks[i:i+100]
notion.blocks.children.append(
block_id=formatted_id,
children=batch
)
return True
except Exception as e:
print(f"Error appending to page: {e}")
return False
def clear_page_content(notion: Client, page_id: str) -> bool:
"""Clear all content from a page."""
try:
formatted_id = format_id_with_dashes(page_id)
# Get all child blocks
children = notion.blocks.children.list(block_id=formatted_id)
# Delete each block
for block in children.get('results', []):
notion.blocks.delete(block_id=block['id'])
return True
except Exception as e:
print(f"Error clearing page: {e}")
return False
def create_database_row(notion: Client, database_id: str, properties: Dict, content_blocks: List[Dict] = None) -> Optional[str]:
"""Create a new row in a Notion database."""
try:
formatted_id = format_id_with_dashes(database_id)
page_data = {
"parent": {"database_id": formatted_id},
"properties": properties
}
if content_blocks:
page_data["children"] = content_blocks[:100] # Limit on creation
result = notion.pages.create(**page_data)
# If more than 100 blocks, append the rest
if content_blocks and len(content_blocks) > 100:
append_to_page(notion, result['id'], content_blocks[100:])
return result['id']
except Exception as e:
print(f"Error creating database row: {e}")
return None
def write_to_page(notion: Client, page_id: str, markdown_content: str, mode: str = 'append') -> bool:
"""Write markdown content to a Notion page."""
blocks = markdown_to_notion_blocks(markdown_content)
if not blocks:
print("No content to write")
return False
if mode == 'replace':
if not clear_page_content(notion, page_id):
return False
return append_to_page(notion, page_id, blocks)
def main():
parser = argparse.ArgumentParser(
description='Push markdown content to Notion pages or databases',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
# Append markdown file to a page
python notion_writer.py --page https://notion.so/My-Page-abc123 --file content.md
# Replace page content
python notion_writer.py --page PAGE_ID --file content.md --replace
# Pipe content from stdin
cat content.md | python notion_writer.py --page PAGE_ID --stdin
# Create database row with title
python notion_writer.py --database DB_URL --title "New Entry" --file content.md
# Test connection
python notion_writer.py --test
'''
)
parser.add_argument('--page', '-p', help='Notion page URL or ID')
parser.add_argument('--database', '-d', help='Notion database URL or ID')
parser.add_argument('--file', '-f', help='Markdown file to push')
parser.add_argument('--stdin', action='store_true', help='Read content from stdin')
parser.add_argument('--replace', '-r', action='store_true', help='Replace page content instead of append')
parser.add_argument('--title', '-t', help='Title for database row (required for database)')
parser.add_argument('--test', action='store_true', help='Test Notion API connection')
parser.add_argument('--list', '-l', nargs='?', const='all', choices=['all', 'pages', 'databases'],
help='List accessible pages and/or databases (default: all)')
parser.add_argument('--info', action='store_true', help='Show page/database info')
args = parser.parse_args()
if not NOTION_TOKEN:
print("Error: NOTION_API_KEY not set in .env file")
print("Get your integration token from: https://www.notion.so/my-integrations")
sys.exit(1)
notion = Client(auth=NOTION_TOKEN)
# Test connection
if args.test:
try:
me = notion.users.me()
print("=" * 50)
print("Notion API Connection Test")
print("=" * 50)
print(f"\n✅ Connected successfully!")
print(f" Bot: {me.get('name', 'Unknown')}")
print(f" Type: {me.get('type', 'Unknown')}")
print("\n" + "=" * 50)
return
except Exception as e:
print(f"❌ Connection failed: {e}")
sys.exit(1)
# List accessible content
if args.list:
filter_map = {'all': 'all', 'pages': 'page', 'databases': 'database'}
list_accessible_content(notion, filter_map.get(args.list, 'all'))
return
# Show info
if args.info:
if args.page:
page_id = extract_notion_id(args.page)
if page_id:
info = get_page_info(notion, page_id)
if info:
print("=" * 50)
print("Page Information")
print("=" * 50)
title = ""
if 'properties' in info:
for prop in info['properties'].values():
if prop.get('type') == 'title':
title_arr = prop.get('title', [])
if title_arr:
title = title_arr[0].get('plain_text', '')
print(f"Title: {title}")
print(f"ID: {info['id']}")
print(f"Created: {info.get('created_time', 'N/A')}")
print(f"URL: {info.get('url', 'N/A')}")
return
if args.database:
db_id = extract_notion_id(args.database)
if db_id:
info = get_database_info(notion, db_id)
if info:
print("=" * 50)
print("Database Information")
print("=" * 50)
title = ""
if info.get('title'):
title = info['title'][0].get('plain_text', '') if info['title'] else ''
print(f"Title: {title}")
print(f"ID: {info['id']}")
print(f"Properties: {', '.join(info.get('properties', {}).keys())}")
return
print("Please specify --page or --database with --info")
return
# Get content
content = None
if args.stdin:
content = sys.stdin.read()
elif args.file:
file_path = Path(args.file)
if not file_path.exists():
print(f"Error: File not found: {args.file}")
sys.exit(1)
content = file_path.read_text(encoding='utf-8')
# Write to page
if args.page:
if not content:
print("Error: No content provided. Use --file or --stdin")
sys.exit(1)
page_id = extract_notion_id(args.page)
if not page_id:
print(f"Error: Invalid Notion page URL/ID: {args.page}")
sys.exit(1)
mode = 'replace' if args.replace else 'append'
print(f"{'Replacing' if mode == 'replace' else 'Appending'} content to page...")
if write_to_page(notion, page_id, content, mode):
print(f"✅ Successfully wrote content to page")
formatted_id = format_id_with_dashes(page_id)
print(f" https://notion.so/{formatted_id.replace('-', '')}")
else:
print("❌ Failed to write content")
sys.exit(1)
return
# Create database row
if args.database:
if not content and not args.title:
print("Error: Provide --title and/or --file for database row")
sys.exit(1)
db_id = extract_notion_id(args.database)
if not db_id:
print(f"Error: Invalid Notion database URL/ID: {args.database}")
sys.exit(1)
# Get database schema to find title property
db_info = get_database_info(notion, db_id)
if not db_info:
sys.exit(1)
# Find the title property
title_prop = None
for prop_name, prop_config in db_info.get('properties', {}).items():
if prop_config.get('type') == 'title':
title_prop = prop_name
break
if not title_prop:
print("Error: Could not find title property in database")
sys.exit(1)
properties = {
title_prop: {
"title": [{"text": {"content": args.title or "Untitled"}}]
}
}
content_blocks = markdown_to_notion_blocks(content) if content else None
print(f"Creating database row...")
row_id = create_database_row(notion, db_id, properties, content_blocks)
if row_id:
print(f"✅ Successfully created database row")
print(f" https://notion.so/{row_id.replace('-', '')}")
else:
print("❌ Failed to create database row")
sys.exit(1)
return
# No action specified
parser.print_help()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,86 @@
---
name: notion-writer
description: Push markdown content to Notion pages or databases. Supports appending to pages, replacing page content, creating database rows, listing accessible content, and getting page/database info. Use when working with Notion documentation, saving reports to Notion, or managing Notion content programmatically.
allowed-tools: Read, Glob, Grep, Write, Edit, Bash
---
# Notion Writer Skill
Push markdown content to Notion pages or databases via Claude Code.
## Prerequisites
- Python virtual environment at `~/Project/claude-skills-factory/custom-skills/02-notion-writer/code/scripts/venv`
- Notion API key configured in `.env` file
- Target pages/databases must be shared with the integration
## Quick Start
```bash
cd ~/Project/claude-skills-factory/custom-skills/02-notion-writer/code/scripts
source venv/bin/activate
```
## Commands
### Test Connection
```bash
python notion_writer.py --test
```
### List Accessible Content
```bash
python notion_writer.py --list
python notion_writer.py --list --filter pages
python notion_writer.py --list --filter databases
```
### Get Page/Database Info
```bash
python notion_writer.py -p PAGE_URL --info
python notion_writer.py -d DATABASE_URL --info
```
### Write to Page
```bash
# Append content
python notion_writer.py -p PAGE_URL -f content.md
# Replace content
python notion_writer.py -p PAGE_URL -f content.md --replace
# From stdin
cat report.md | python notion_writer.py -p PAGE_URL --stdin
```
### Create Database Row
```bash
python notion_writer.py -d DATABASE_URL -t "Entry Title" -f content.md
```
## Supported Markdown
| Markdown | Notion Block |
|----------|--------------|
| `# Heading` | Heading 1 |
| `## Heading` | Heading 2 |
| `### Heading` | Heading 3 |
| `- item` | Bulleted list |
| `1. item` | Numbered list |
| `- [ ] task` | To-do (unchecked) |
| `- [x] task` | To-do (checked) |
| `> quote` | Quote |
| `` ```code``` `` | Code block |
| `---` | Divider |
| Paragraphs | Paragraph |
## Workflow Example
Integrate with Jamie YouTube Manager to log video info:
```bash
# Check video and save to markdown
python jamie_youtube_api_test.py VIDEO_URL
# Write to Notion
python notion_writer.py -p LOG_PAGE_URL -f output/video_status.md
```

View File

@@ -3,7 +3,7 @@
> **Purpose**: YouTube SEO Auditor & Content Manager for Jamie Plastic Surgery Clinic (제이미성형외과)
> **Platform**: Claude Code (CLI)
> **Input**: YouTube URLs, video metadata, or exported data
> **Output**: Audit reports, optimized metadata, schema markup
> **Output**: Audit reports, optimized metadata, schema markup, API batch updates
---
@@ -17,6 +17,114 @@
| Schema Generation | Video details | JSON-LD markup |
| Description Writing | Video topic | SEO-optimized description |
| Shorts Optimization | Shorts content | Optimization checklist |
| **Batch Metadata Update** | T&D document | YouTube API batch update |
| **Video Info Fetch** | YouTube URL(s) | Detailed video info + stats |
| **API Connection Test** | OAuth credentials | Connection status |
---
## YouTube API Integration
### Prerequisites
1. **Google Cloud Project**: `ourdigital-insights`
2. **YouTube Data API v3**: Enabled
3. **OAuth Credentials**: Desktop app type
### Setup
```bash
# Navigate to scripts directory
cd ~/Project/claude-skills-factory/custom-skills/43-jamie-youtube-manager/code/scripts
# Activate virtual environment
source venv/bin/activate
# Required packages (already installed)
pip install google-api-python-client google-auth-oauthlib python-dotenv
```
### Environment Configuration
`.env` file structure:
```
GOOGLE_CLIENT_ID=<your-client-id>
GOOGLE_CLIENT_SECRET=<your-client-secret>
GOOGLE_PROJECT_ID=ourdigital-insights
GOOGLE_CLIENT_SECRETS_FILE=/Users/ourdigital/.config/gcloud/keys/jamie-youtube-manager.json
```
### API Scripts
#### 1. Connection Test (`jamie_youtube_api_test.py`)
Tests OAuth authentication and video access:
```bash
source jamie_youtube_venv/bin/activate
python jamie_youtube_api_test.py
```
**Output**:
- Authenticated channel info
- Video access verification
- Credential status
#### 2. Video Info (`jamie_video_info.py`)
Fetches detailed video information from URLs or video IDs:
```bash
# Single video by URL
python jamie_video_info.py https://youtu.be/P-ovr-aaD1E
# Multiple videos
python jamie_video_info.py URL1 URL2 URL3
# Verbose mode (includes description & tags)
python jamie_video_info.py VIDEO_ID -v
# JSON output
python jamie_video_info.py VIDEO_ID --json
```
**Output**:
- Video title, URL, channel
- Published date, privacy status, duration
- Statistics (views, likes, comments)
- Jamie channel badge (🏥 Jamie vs External)
- Description and tags (verbose mode)
#### 3. Channel Status (`jamie_channel_status.py`)
Check current status of Jamie YouTube channel and all 18 videos:
```bash
python jamie_channel_status.py
```
**Output**:
- Channel statistics (subscribers, views, video count)
- All 18 video status (title, privacy, views, duration)
- Summary by privacy status
#### 4. Batch Metadata Update (`jamie_youtube_batch_update.py`)
Updates video titles and descriptions via YouTube API:
```bash
# Dry-run mode (preview only)
python jamie_youtube_batch_update.py --dry-run
# Execute actual updates
python jamie_youtube_batch_update.py --execute
# Update specific video
python jamie_youtube_batch_update.py --execute --video-id VIDEO_ID
```
**Features**:
- Dry-run mode for safe testing
- Batch update all 18 Jamie videos
- Common footer auto-appended
- OAuth token persistence
---
@@ -348,4 +456,83 @@ User: "영어 자막/메타데이터 추천해줘"
---
*Version 1.0.0 | Claude Code | 2025-12*
## File Structure
```
43-jamie-youtube-manager/
├── code/
│ ├── CLAUDE.md # This file (Claude Code skill)
│ ├── scripts/
│ │ ├── jamie_youtube_api_test.py # API connection test
│ │ ├── jamie_video_info.py # Video info fetcher (URL-based)
│ │ ├── jamie_channel_status.py # Channel & video status
│ │ ├── jamie_youtube_batch_update.py # Batch metadata updater
│ │ ├── jamie_youtube_token.pickle # OAuth token (cached)
│ │ ├── venv/ # Python virtual environment
│ │ └── .env # Environment variables
│ ├── output/
│ │ └── jamie_youtube_td_final.md # T&D document (18 videos)
│ └── references/
│ └── ...
└── desktop/
├── SKILL.md # Claude Desktop skill
└── references/
└── ...
```
---
## Video Inventory (18 Videos)
| No | Video ID | 시술명 | 길이 |
|---|---|---|---|
| 0 | P-ovr-aaD1E | 병원 소개 | 0:33 |
| 1 | qZQwAX6Onj0 | 눈 성형 | 1:27 |
| 2 | _m6H4F_nLYU | 퀵 매몰법 | 1:28 |
| 3 | CBAGAY_b0HU | 하이브리드 쌍꺼풀 | 1:33 |
| 4 | TxFajDli1QQ | 안검하수 눈매교정술 | 1:53 |
| 5 | Ey5eR4dCi_I | 눈밑지방 재배치 | 1:38 |
| 6 | ffUmrE-Ckt0 | 듀얼 트임 수술 | 1:42 |
| 7 | 1MA0OJJYcQk | 눈썹밑 피부절개술 | 1:33 |
| 8 | UoeOnT1j41Y | 눈 재수술 | 1:59 |
| 9 | a7FcFMiGiTs | 이마 성형 | 3:44 |
| 10 | lIq816rp4js | 내시경 이마 거상술 | 3:42 |
| 11 | EwgtJUH46dc | 내시경 눈썹 거상술 | 3:50 |
| 12 | gfbJlqlAIfg | 동안 성형 | 1:51 |
| 13 | lRtAatuhcC4 | 동안 시술 | 2:21 |
| 14 | 7saghBp2a_A | 앞광대 리프팅 | 1:44 |
| 15 | Mq6zcx_8owY | 스마스 리프팅 | 1:56 |
| 16 | _bCJDZx2L2I | 자가 지방이식 | 1:47 |
| 17 | kXbP1T6ICxY | 하이푸 리프팅 | 1:50 |
---
## Quick Reference Commands
```bash
# Navigate to scripts directory
cd ~/Project/claude-skills-factory/custom-skills/43-jamie-youtube-manager/code/scripts
# Activate environment
source venv/bin/activate
# Test API connection
python jamie_youtube_api_test.py
# Get specific video info from URL
python jamie_video_info.py https://youtu.be/VIDEO_ID -v
# Check channel & video status
python jamie_channel_status.py
# Preview batch update
python jamie_youtube_batch_update.py --dry-run
# Execute batch update
python jamie_youtube_batch_update.py --execute
```
---
*Version 1.1.0 | Claude Code | 2025-12-26*
*Added: YouTube Data API v3 integration, batch metadata update*

View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
Jamie YouTube Channel Status Check
Fetches current status of all Jamie clinic videos.
"""
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
import pickle
# Load environment variables
load_dotenv(Path(__file__).parent / '.env')
SCOPES = ['https://www.googleapis.com/auth/youtube.force-ssl']
TOKEN_FILE = Path(__file__).parent / 'jamie_youtube_token.pickle'
CLIENT_SECRETS_FILE = Path(os.getenv('GOOGLE_CLIENT_SECRETS_FILE',
'/Users/ourdigital/.config/gcloud/keys/jamie-youtube-manager.json'))
# Jamie video IDs
JAMIE_VIDEOS = [
"P-ovr-aaD1E", # 병원 소개
"qZQwAX6Onj0", # 눈 성형
"_m6H4F_nLYU", # 퀵 매몰법
"CBAGAY_b0HU", # 하이브리드 쌍꺼풀
"TxFajDli1QQ", # 안검하수 눈매교정술
"Ey5eR4dCi_I", # 눈밑지방 재배치
"ffUmrE-Ckt0", # 듀얼 트임 수술
"1MA0OJJYcQk", # 눈썹밑 피부절개술
"UoeOnT1j41Y", # 눈 재수술
"a7FcFMiGiTs", # 이마 성형
"lIq816rp4js", # 내시경 이마 거상술
"EwgtJUH46dc", # 내시경 눈썹 거상술
"gfbJlqlAIfg", # 동안 성형
"lRtAatuhcC4", # 동안 시술
"7saghBp2a_A", # 앞광대 리프팅
"Mq6zcx_8owY", # 스마스 리프팅
"_bCJDZx2L2I", # 자가 지방이식
"kXbP1T6ICxY", # 하이푸 리프팅
]
def get_authenticated_service():
"""Authenticate and return YouTube API service."""
creds = None
if TOKEN_FILE.exists():
with open(TOKEN_FILE, 'rb') as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRETS_FILE, SCOPES)
creds = flow.run_local_server(port=8080)
with open(TOKEN_FILE, 'wb') as token:
pickle.dump(creds, token)
return build('youtube', 'v3', credentials=creds)
def get_channel_info(youtube, channel_id):
"""Get channel information."""
response = youtube.channels().list(
part="snippet,statistics,brandingSettings",
id=channel_id
).execute()
if response.get('items'):
return response['items'][0]
return None
def get_videos_status(youtube, video_ids):
"""Get status of multiple videos."""
# YouTube API allows max 50 videos per request
videos = []
for i in range(0, len(video_ids), 50):
batch = video_ids[i:i+50]
response = youtube.videos().list(
part="snippet,status,statistics,contentDetails",
id=",".join(batch)
).execute()
videos.extend(response.get('items', []))
return videos
def format_duration(duration):
"""Convert ISO 8601 duration to readable format."""
import re
match = re.match(r'PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?', duration)
if match:
hours, minutes, seconds = match.groups()
parts = []
if hours:
parts.append(f"{hours}h")
if minutes:
parts.append(f"{minutes}m")
if seconds:
parts.append(f"{seconds}s")
return " ".join(parts) if parts else "0s"
return duration
def main():
print("=" * 70)
print("Jamie Clinic (제이미성형외과) - YouTube Channel Status")
print("=" * 70)
youtube = get_authenticated_service()
# Get all Jamie videos
videos = get_videos_status(youtube, JAMIE_VIDEOS)
if not videos:
print("\n❌ No videos found or accessible")
return
# Get channel info from first video
channel_id = videos[0]['snippet']['channelId']
channel = get_channel_info(youtube, channel_id)
if channel:
stats = channel.get('statistics', {})
print(f"\n📺 Channel: {channel['snippet']['title']}")
print(f" ID: {channel_id}")
print(f" Subscribers: {stats.get('subscriberCount', 'Hidden')}")
print(f" Total Views: {stats.get('viewCount', '0')}")
print(f" Total Videos: {stats.get('videoCount', '0')}")
print("\n" + "-" * 70)
print(f"{'No':<3} {'Title':<40} {'Status':<10} {'Views':<8} {'Duration'}")
print("-" * 70)
total_views = 0
status_counts = {'public': 0, 'unlisted': 0, 'private': 0}
for i, video in enumerate(videos):
snippet = video['snippet']
status = video['status']['privacyStatus']
stats = video.get('statistics', {})
views = int(stats.get('viewCount', 0))
duration = format_duration(video['contentDetails']['duration'])
title = snippet['title'][:38] + '..' if len(snippet['title']) > 40 else snippet['title']
status_icon = {'public': '🟢', 'unlisted': '🟡', 'private': '🔴'}.get(status, '')
print(f"{i+1:<3} {title:<40} {status_icon} {status:<8} {views:<8} {duration}")
total_views += views
status_counts[status] = status_counts.get(status, 0) + 1
print("-" * 70)
print(f"\n📊 Summary")
print(f" Total Videos: {len(videos)}")
print(f" Total Views: {total_views:,}")
print(f" Public: {status_counts.get('public', 0)} | Unlisted: {status_counts.get('unlisted', 0)} | Private: {status_counts.get('private', 0)}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""
Jamie YouTube Video Info Fetcher
Fetches detailed information for specific YouTube videos from URLs.
"""
import os
import sys
import re
import argparse
from pathlib import Path
from datetime import datetime
from dotenv import load_dotenv
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
import pickle
# Load environment variables
load_dotenv(Path(__file__).parent / '.env')
SCOPES = ['https://www.googleapis.com/auth/youtube.force-ssl']
TOKEN_FILE = Path(__file__).parent / 'jamie_youtube_token.pickle'
CLIENT_SECRETS_FILE = Path(os.getenv('GOOGLE_CLIENT_SECRETS_FILE',
'/Users/ourdigital/.config/gcloud/keys/jamie-youtube-manager.json'))
# Jamie Clinic YouTube Channel (Default)
JAMIE_CHANNEL_ID = "UCtjR6NnlaX1dPPER7wHbGpw"
JAMIE_CHANNEL_NAME = "제이미 성형외과"
def extract_video_id(url_or_id):
"""Extract video ID from various YouTube URL formats."""
# Already a video ID
if re.match(r'^[\w-]{11}$', url_or_id):
return url_or_id
# Standard YouTube URLs
patterns = [
r'(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/|youtube\.com/v/)([^&\n?#]+)',
r'youtube\.com/shorts/([^&\n?#]+)',
]
for pattern in patterns:
match = re.search(pattern, url_or_id)
if match:
return match.group(1)
return None
def get_authenticated_service():
"""Authenticate and return YouTube API service."""
creds = None
if TOKEN_FILE.exists():
with open(TOKEN_FILE, 'rb') as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRETS_FILE, SCOPES)
creds = flow.run_local_server(port=8080)
with open(TOKEN_FILE, 'wb') as token:
pickle.dump(creds, token)
return build('youtube', 'v3', credentials=creds)
def format_duration(duration):
"""Convert ISO 8601 duration to readable format."""
match = re.match(r'PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?', duration)
if match:
hours, minutes, seconds = match.groups()
parts = []
if hours:
parts.append(f"{hours}h")
if minutes:
parts.append(f"{int(minutes)}m")
if seconds:
parts.append(f"{int(seconds)}s")
return " ".join(parts) if parts else "0s"
return duration
def format_number(num_str):
"""Format number with commas."""
try:
return f"{int(num_str):,}"
except:
return num_str
def get_video_info(youtube, video_id, verbose=False):
"""Fetch detailed video information."""
try:
response = youtube.videos().list(
part="snippet,status,statistics,contentDetails,topicDetails",
id=video_id
).execute()
if not response.get('items'):
return None
video = response['items'][0]
snippet = video['snippet']
status = video['status']
stats = video.get('statistics', {})
content = video['contentDetails']
# Check if Jamie video
is_jamie = snippet['channelId'] == JAMIE_CHANNEL_ID
info = {
'id': video_id,
'title': snippet['title'],
'description': snippet['description'],
'channel': snippet['channelTitle'],
'channel_id': snippet['channelId'],
'is_jamie': is_jamie,
'published_at': snippet['publishedAt'],
'privacy_status': status['privacyStatus'],
'duration': format_duration(content['duration']),
'duration_raw': content['duration'],
'views': format_number(stats.get('viewCount', '0')),
'likes': format_number(stats.get('likeCount', '0')),
'comments': format_number(stats.get('commentCount', '0')),
'tags': snippet.get('tags', []),
'category_id': snippet.get('categoryId', ''),
'thumbnail': snippet.get('thumbnails', {}).get('maxres', {}).get('url') or
snippet.get('thumbnails', {}).get('high', {}).get('url', ''),
}
return info
except Exception as e:
print(f"Error fetching video info: {e}")
return None
def print_video_info(info, verbose=False):
"""Print video information in formatted output."""
if not info:
print("❌ Video not found or not accessible")
return
jamie_badge = "🏥 Jamie" if info['is_jamie'] else "External"
status_icon = {'public': '🟢', 'unlisted': '🟡', 'private': '🔴'}.get(info['privacy_status'], '')
print("\n" + "="*70)
print(f"📹 Video Information")
print("="*70)
print(f"\n🎬 Title: {info['title']}")
print(f"🔗 URL: https://www.youtube.com/watch?v={info['id']}")
print(f"📺 Channel: {info['channel']} [{jamie_badge}]")
print(f"📅 Published: {info['published_at'][:10]}")
print(f"{status_icon} Status: {info['privacy_status']}")
print(f"⏱️ Duration: {info['duration']}")
print(f"\n📊 Statistics:")
print(f" Views: {info['views']}")
print(f" Likes: {info['likes']}")
print(f" Comments: {info['comments']}")
if verbose:
print(f"\n📝 Description:")
print("-"*70)
desc_lines = info['description'].split('\n')[:15]
for line in desc_lines:
print(f" {line[:65]}")
if len(info['description'].split('\n')) > 15:
print(" ...")
print("-"*70)
if info['tags']:
print(f"\n🏷️ Tags ({len(info['tags'])}):")
print(f" {', '.join(info['tags'][:10])}")
if len(info['tags']) > 10:
print(f" ... and {len(info['tags']) - 10} more")
print(f"\n🖼️ Thumbnail: {info['thumbnail']}")
print("\n" + "="*70)
def main():
parser = argparse.ArgumentParser(
description='Fetch YouTube video information',
epilog='Examples:\n'
' python jamie_video_info.py https://youtu.be/P-ovr-aaD1E\n'
' python jamie_video_info.py P-ovr-aaD1E -v\n'
' python jamie_video_info.py URL1 URL2 URL3',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument('urls', nargs='+', help='YouTube URL(s) or video ID(s)')
parser.add_argument('-v', '--verbose', action='store_true', help='Show detailed info including description and tags')
parser.add_argument('--json', action='store_true', help='Output as JSON')
args = parser.parse_args()
youtube = get_authenticated_service()
if not youtube:
sys.exit(1)
results = []
for url in args.urls:
video_id = extract_video_id(url)
if not video_id:
print(f"\n❌ Invalid URL or video ID: {url}")
continue
info = get_video_info(youtube, video_id, args.verbose)
if args.json:
results.append(info)
else:
print_video_info(info, args.verbose)
if args.json:
import json
print(json.dumps(results, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""
YouTube API Connection Test Script for Jamie Clinic
Tests OAuth authentication and verifies Jamie channel access.
"""
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
import pickle
# Load environment variables
load_dotenv(Path(__file__).parent / '.env')
# YouTube API scopes needed for updating video metadata
SCOPES = ['https://www.googleapis.com/auth/youtube.force-ssl']
# Token file path
TOKEN_FILE = Path(__file__).parent / 'jamie_youtube_token.pickle'
CLIENT_SECRETS_FILE = Path(os.getenv('GOOGLE_CLIENT_SECRETS_FILE',
'/Users/ourdigital/.config/gcloud/keys/jamie-youtube-manager.json'))
# Jamie Clinic YouTube Channel (Default)
JAMIE_CHANNEL_ID = "UCtjR6NnlaX1dPPER7wHbGpw"
JAMIE_CHANNEL_NAME = "제이미 성형외과"
def get_authenticated_service():
"""Authenticate and return YouTube API service."""
creds = None
if TOKEN_FILE.exists():
with open(TOKEN_FILE, 'rb') as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
print("Refreshing expired credentials...")
creds.refresh(Request())
else:
if not CLIENT_SECRETS_FILE.exists():
print(f"\n[ERROR] OAuth client secret file not found!")
print(f"Expected location: {CLIENT_SECRETS_FILE}")
return None
print("Starting OAuth authentication flow...")
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRETS_FILE, SCOPES)
creds = flow.run_local_server(port=8080)
with open(TOKEN_FILE, 'wb') as token:
pickle.dump(creds, token)
print(f"Credentials saved to: {TOKEN_FILE}")
return build('youtube', 'v3', credentials=creds)
def test_jamie_channel(youtube):
"""Test connection to Jamie's YouTube channel."""
print("\n" + "="*60)
print(f"Testing Jamie Channel Access: {JAMIE_CHANNEL_NAME}")
print("="*60)
try:
response = youtube.channels().list(
part="snippet,statistics,brandingSettings",
id=JAMIE_CHANNEL_ID
).execute()
if response.get('items'):
channel = response['items'][0]
stats = channel.get('statistics', {})
print(f"\n✅ Jamie Channel Connected!")
print(f"\n📺 Channel Info:")
print(f" Name: {channel['snippet']['title']}")
print(f" ID: {JAMIE_CHANNEL_ID}")
print(f" Subscribers: {stats.get('subscriberCount', 'Hidden')}")
print(f" Total Views: {stats.get('viewCount', '0')}")
print(f" Total Videos: {stats.get('videoCount', '0')}")
return True
else:
print(f"\n❌ Jamie channel not found")
return False
except Exception as e:
print(f"\n❌ Connection failed: {e}")
return False
def test_video_access(youtube, video_id):
"""Test if we can access a Jamie video."""
print(f"\n" + "="*60)
print(f"Testing Video Access: {video_id}")
print("="*60)
try:
response = youtube.videos().list(
part="snippet,status,statistics",
id=video_id
).execute()
if response.get('items'):
video = response['items'][0]
snippet = video['snippet']
stats = video.get('statistics', {})
# Verify it's a Jamie video
is_jamie = snippet['channelId'] == JAMIE_CHANNEL_ID
print(f"\n✅ Video accessible!")
print(f" Title: {snippet['title']}")
print(f" Channel: {snippet['channelTitle']}")
print(f" Status: {video['status']['privacyStatus']}")
print(f" Views: {stats.get('viewCount', '0')}")
print(f" Jamie Channel: {'✅ Yes' if is_jamie else '❌ No'}")
return True
else:
print(f"\n⚠️ Video not found or not accessible")
return False
except Exception as e:
print(f"\n❌ Video access failed: {e}")
return False
def main():
print("="*60)
print(f"Jamie Clinic ({JAMIE_CHANNEL_NAME}) - API Connection Test")
print("="*60)
youtube = get_authenticated_service()
if not youtube:
sys.exit(1)
# Test Jamie channel access
if not test_jamie_channel(youtube):
sys.exit(1)
# Test access to Jamie's intro video
test_video_id = "P-ovr-aaD1E"
test_video_access(youtube, test_video_id)
print("\n" + "="*60)
print("API Connection Test Complete!")
print("="*60)
print("\nAvailable commands:")
print(" python jamie_channel_status.py # Full channel status")
print(" python jamie_video_info.py <URL> # Get video info")
print(" python jamie_youtube_batch_update.py --dry-run")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,705 @@
#!/usr/bin/env python3
"""
YouTube Batch Metadata Update Script for Jamie Clinic
Updates video titles and descriptions for all Jamie videos.
"""
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
import pickle
# Load environment variables
load_dotenv(Path(__file__).parent / '.env')
# YouTube API scopes
SCOPES = ['https://www.googleapis.com/auth/youtube.force-ssl']
# Token file path
TOKEN_FILE = Path(__file__).parent / 'jamie_youtube_token.pickle'
CLIENT_SECRETS_FILE = Path(os.getenv('GOOGLE_CLIENT_SECRETS_FILE',
'/Users/ourdigital/.config/gcloud/keys/jamie-youtube-manager.json'))
# Common footer for all video descriptions
COMMON_FOOTER = """━━━━━━━━━━━━━━━
🏥 제이미 성형외과
📍 서울 강남구 압구정로 136 EHL빌딩 3층 (압구정역 5번출구 도보 5분)
📞 02-542-2399
🌐 https://jamie.clinic
📧 info@jamie.clinic
━━━━━━━━━━━━━━━
⏰ 진료시간
월-금 10:00-19:00 | 토 10:00-16:00 | 일·공휴일 휴진
(점심시간 13:00-14:00)
━━━━━━━━━━━━━━━
※ 모든 수술 및 시술은 개인에 따라 출혈, 감염, 염증 등의 부작용이 발생할 수 있으며, 결과에는 개인차가 있을 수 있으므로 의료진과 충분한 상담 후 결정하시기 바랍니다.
#제이미성형외과 #압구정성형외과 #강남성형외과 #정기호원장"""
# Video metadata - parsed from jamie_youtube_td_final.md
VIDEOS = [
{
"id": "P-ovr-aaD1E",
"title": "제이미 성형외과 소개 | 정기호 원장 인사말",
"description": """압구정역 5번출구에 위치한 제이미성형외과를 소개합니다.
눈, 이마, 동안 성형 전문 정기호 원장이 직접 인사드립니다.
📌 제이미 성형외과 전문 분야
• 눈 성형 (쌍꺼풀, 눈매교정, 눈밑, 트임 수술)
• 이마 성형 (내시경 이마/눈썹 거상술)
• 동안 성형 (리프팅, 지방이식)
#제이미성형외과소개 #압구정성형외과 #정기호원장"""
},
{
"id": "qZQwAX6Onj0",
"title": "눈 성형, 자연스러운 눈매를 만드는 방법 | 제이미 성형외과 정기호 원장",
"description": """제이미 성형외과에서 진행하는 다양한 눈 성형 수술에 대해 정기호 원장이 설명해 드립니다.
📌 제이미 눈 성형 종류
• 퀵 매몰법 - 티 안 나게 자연스러운 쌍꺼풀
• 하이브리드 쌍꺼풀 - 절개법+매몰법 장점 결합
• 안검하수 눈매교정술 - 졸린 눈을 또렷하게
• 눈밑지방 재배치 - 다크서클과 눈밑 꺼짐 개선
• 듀얼 트임 수술 - 앞트임+뒤트임으로 시원한 눈매
• 눈썹밑 피부절개술 - 처진 눈꺼풀 개선
• 눈 재수술 - 이전 수술의 아쉬움 교정
📌 이런 분들께 추천드립니다
• 자연스러운 쌍꺼풀을 원하시는 분
• 눈이 작고 답답해 보이는 분
• 눈꺼풀이 처지거나 눈밑이 어두운 분
• 이전 눈 수술 결과가 만족스럽지 않은 분
⏱️ 챕터
0:00 인트로
0:10 눈 성형의 종류
0:35 수술별 특징 설명
1:10 상담 안내
#눈성형 #쌍꺼풀수술 #눈매교정 #압구정눈성형"""
},
{
"id": "_m6H4F_nLYU",
"title": "퀵 매몰법, 티 안 나게 자연스러운 쌍꺼풀 | 제이미 성형외과 정기호 원장",
"description": """휴가를 내지 않고도 자연스러운 쌍꺼풀을 만들 수 있는 퀵 매몰법에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 자연스러운 쌍꺼풀 라인을 원하시는 분
• 수술 후 빠른 일상 복귀가 필요하신 분
• 절개 없이 쌍꺼풀 수술을 받고 싶으신 분
• 붓기와 멍이 적은 수술을 원하시는 분
📌 제이미 퀵 매몰법 특징
• 수술 시간 10-15분
• 수면마취 + 국소마취 병행
• 수술 다음 날부터 세안, 화장 가능
• 실밥 제거 없음
⏱️ 챕터
0:00 인트로
0:08 퀵 매몰법이란?
0:30 수술 방법 설명
0:55 회복 과정 안내
1:15 자주 묻는 질문
#퀵매몰법 #매몰법 #쌍꺼풀수술 #자연유착 #눈성형"""
},
{
"id": "CBAGAY_b0HU",
"title": "하이브리드 쌍꺼풀, 절개법+매몰법 장점만 결합 | 제이미 성형외과 정기호 원장",
"description": """절개법의 또렷함과 매몰법의 자연스러움을 결합한 하이브리드 쌍꺼풀에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 매몰법으로는 유지가 어려운 두꺼운 눈꺼풀을 가지신 분
• 절개법의 흉터가 걱정되시는 분
• 또렷하면서도 자연스러운 라인을 원하시는 분
• 이전 매몰법 후 풀린 경험이 있으신 분
📌 하이브리드 쌍꺼풀 특징
• 최소 절개로 또렷한 라인 형성
• 매몰법보다 유지력 우수
• 절개법보다 회복 기간 단축
• 개인별 눈 상태에 맞춘 맞춤 디자인
⏱️ 챕터
0:00 인트로
0:10 하이브리드 쌍꺼풀이란?
0:30 매몰법·절개법과의 차이
0:55 수술 과정 설명
1:20 회복 기간 안내
#하이브리드쌍꺼풀 #쌍꺼풀수술 #절개법 #매몰법 #눈성형"""
},
{
"id": "TxFajDli1QQ",
"title": "안검하수 눈매교정술, 졸린 눈을 또렷하게 | 제이미 성형외과 정기호 원장",
"description": """졸리고 답답한 눈매를 또렷하고 시원하게 개선하는 안검하수 눈매교정술에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 눈을 떠도 졸려 보인다는 말을 자주 듣는 분
• 이마에 힘을 줘야 눈이 떠지는 분
• 눈꺼풀이 무겁고 답답하게 느껴지는 분
• 쌍꺼풀 수술 후에도 눈이 작아 보이는 분
📌 안검하수란?
눈을 뜨는 근육(눈꺼풀올림근)의 힘이 약해 눈꺼풀이 처지는 현상입니다. 선천적 또는 후천적으로 발생할 수 있으며, 눈매교정술을 통해 개선이 가능합니다.
📌 제이미 눈매교정술 특징
• 정밀한 진단을 통한 개인 맞춤 교정
• 자연스러운 눈 뜨는 힘 회복
• 쌍꺼풀 수술과 동시 진행 가능
⏱️ 챕터
0:00 인트로
0:10 안검하수란 무엇인가?
0:35 자가 진단 방법
1:00 수술 방법 설명
1:30 회복 과정 및 주의사항
#안검하수 #눈매교정 #눈매교정술 #졸린눈 #눈성형"""
},
{
"id": "Ey5eR4dCi_I",
"title": "눈밑지방 재배치, 다크서클과 눈밑 꺼짐 동시 개선 | 제이미 성형외과 정기호 원장",
"description": """어둡고 칙칙한 눈밑을 환하게 개선하는 눈밑지방 재배치에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 눈밑이 불룩하게 튀어나와 보이는 분
• 눈밑 다크서클이 심한 분
• 눈밑 주름과 그늘로 피곤해 보이는 분
• 눈밑 지방 제거 후 꺼짐이 생긴 분
📌 눈밑지방 재배치란?
튀어나온 눈밑 지방을 제거하지 않고, 꺼진 부위로 재배치하여 눈밑을 평탄하고 환하게 만드는 수술입니다. 지방 제거만 하는 것보다 자연스럽고 오래 유지됩니다.
📌 제이미 눈밑지방 재배치 특징
• 결막 절개로 외부 흉터 없음
• 지방 재배치로 꺼짐 방지
• 다크서클과 눈밑 주름 동시 개선
⏱️ 챕터
0:00 인트로
0:10 눈밑지방 재배치란?
0:30 지방 제거 vs 재배치 차이
0:55 수술 과정 설명
1:25 회복 기간 안내
#눈밑지방재배치 #다크서클 #눈밑수술 #눈밑지방 #눈성형"""
},
{
"id": "ffUmrE-Ckt0",
"title": "듀얼 트임 수술, 앞트임+뒤트임으로 시원한 눈매 완성 | 제이미 성형외과 정기호 원장",
"description": """앞트임과 뒤트임을 함께 진행하여 더욱 시원하고 매력적인 눈매를 만드는 듀얼 트임 수술에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 눈이 작고 답답해 보이는 분
• 눈 사이 거리가 멀어 보이는 분
• 눈꼬리가 올라가 사나운 인상을 주는 분
• 쌍꺼풀 수술만으로는 눈이 충분히 커지지 않는 분
📌 듀얼 트임이란?
앞트임(몽고주름 제거)과 뒤트임(눈꼬리 연장)을 함께 진행하여 눈의 가로 길이를 확장하고 눈매 방향을 조절하는 수술입니다.
📌 제이미 듀얼 트임 특징
• 개인별 눈 형태에 맞춘 트임 범위 결정
• 흉터 최소화를 위한 정밀 봉합
• 쌍꺼풀 수술과 동시 진행 가능
⏱️ 챕터
0:00 인트로
0:10 듀얼 트임이란?
0:30 앞트임·뒤트임 각각의 효과
0:55 수술 방법 설명
1:25 회복 과정 및 흉터 관리
#듀얼트임 #앞트임 #뒤트임 #눈트임 #눈성형"""
},
{
"id": "1MA0OJJYcQk",
"title": "눈썹밑 피부절개술, 티 안 나게 처진 눈꺼풀 개선 | 제이미 성형외과 정기호 원장",
"description": """눈썹 아래 절개로 흉터 걱정 없이 처진 눈꺼풀을 개선하는 눈썹밑 피부절개술에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 나이가 들면서 눈꺼풀이 처진 분
• 기존 쌍꺼풀 라인은 유지하고 싶은 분
• 상안검 수술의 흉터가 걱정되시는 분
• 자연스러운 개선을 원하시는 중장년층
📌 눈썹밑 피부절개술이란?
눈썹 바로 아래 피부를 절개하여 처진 눈꺼풀 피부를 제거하는 수술입니다. 절개선이 눈썹 아래에 위치하여 흉터가 거의 눈에 띄지 않습니다.
📌 제이미 눈썹밑 피부절개술 특징
• 눈썹-속눈썹 경계선에 절개로 흉터 최소화
• 기존 쌍꺼풀 라인 유지 가능
• 이마거상술 대비 회복 기간 단축
⏱️ 챕터
0:00 인트로
0:10 눈썹밑 피부절개술이란?
0:30 상안검 수술과의 차이
0:55 수술 방법 설명
1:20 회복 과정 안내
#눈썹밑절개 #눈꺼풀처짐 #상안검 #눈성형 #중년눈성형"""
},
{
"id": "UoeOnT1j41Y",
"title": "눈 재수술, 이전 수술의 아쉬움을 바로잡는 방법 | 제이미 성형외과 정기호 원장",
"description": """이전 눈 수술 결과가 만족스럽지 않은 분들을 위한 눈 재수술에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 쌍꺼풀이 풀렸거나 비대칭인 분
• 쌍꺼풀 라인이 너무 높거나 두꺼운 분
• 눈매교정 후 눈이 덜 떠지거나 과교정된 분
• 트임 수술 후 흉터나 재유착이 발생한 분
📌 눈 재수술이 어려운 이유
"깨끗한 도화지에 그림을 그리면 화가의 실력이 100% 발휘될 텐데, 재수술은 어느 정도 낙서가 있는 도화지에 덧칠을 하는 것과 같습니다."
📌 제이미 눈 재수술 특징
• 2008년부터 눈 성형을 시행한 풍부한 경험
• 이전 수술 상태에 대한 정밀 분석
• 현실적인 기대치에 대한 솔직한 상담
• 개인별 맞춤 재수술 계획 수립
⏱️ 챕터
0:00 인트로
0:10 눈 재수술이 필요한 경우
0:35 재수술이 어려운 이유
1:05 재수술 방법 및 고려사항
1:40 상담 시 확인해야 할 점
#눈재수술 #쌍꺼풀재수술 #눈성형재수술 #눈수술실패 #눈성형"""
},
{
"id": "a7FcFMiGiTs",
"title": "이마 성형, 이마 주름과 처진 눈썹을 개선하는 방법 | 제이미 성형외과 정기호 원장",
"description": """깊어지는 이마 주름과 처진 눈썹으로 고민하시는 분들을 위한 이마 성형에 대해 정기호 원장이 설명해 드립니다.
📌 제이미 이마 성형 종류
• 내시경 이마 거상술 - 이마 주름과 처진 눈꺼풀 동시 개선
• 내시경 눈썹 거상술 - 낮은 눈썹과 이마를 젊게
📌 이런 분들께 추천드립니다
• 이마 주름이 깊어지신 분
• 눈썹이 내려앉아 피곤해 보이는 분
• 눈꺼풀이 처져 답답한 인상을 주는 분
• 보톡스로는 효과가 부족하신 분
• 눈썹 위치가 낮아 답답한 인상인 분
📌 제이미 이마 성형 특징
• 두피 내 절개로 흉터 노출 없음
• 3점 고정 방식으로 자연스러운 리프팅
• 5년 AS 프로그램 운영
⏱️ 챕터
0:00 인트로
0:15 이마 성형이 필요한 경우
0:45 내시경 이마 거상술 설명
1:30 내시경 눈썹 거상술 설명
2:15 수술 방법 비교
3:00 회복 과정 및 주의사항
3:30 자주 묻는 질문
#이마성형 #이마거상술 #눈썹거상술 #이마주름 #동안성형"""
},
{
"id": "lIq816rp4js",
"title": "내시경 이마거상술, 이마 주름과 처진 눈꺼풀 동시 개선 | 제이미 성형외과 정기호 원장",
"description": """깊어지는 이마 주름과 처진 눈꺼풀로 고민하시는 분들을 위한 내시경 이마거상술에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 이마 주름이 깊어지신 분
• 눈꺼풀이 처져 답답한 인상을 주는 분
• 눈썹이 내려앉아 피곤해 보이는 분
• 보톡스로는 효과가 부족하신 분
📌 내시경 이마거상술이란?
두피 내 3곳의 최소 절개를 통해 내시경으로 이마 조직을 박리한 후, 처진 이마와 눈썹을 위로 당겨 고정하는 수술입니다.
📌 제이미 내시경 이마거상술 특징
• 3점 고정 방식 - "인형극 실이 많을수록 자연스러운 것처럼"
• 흡수성 봉합사 주문 제작 사용
• 두피 절개로 흉터 노출 없음
• 5년 AS 프로그램 운영
⏱️ 챕터
0:00 인트로
0:15 내시경 이마거상술이란?
0:45 3점 고정 방식 설명
1:20 수술 과정 안내
2:00 회복 기간 및 주의사항
2:45 자주 묻는 질문
3:25 마무리
#내시경이마거상술 #이마거상술 #이마주름 #눈썹거상 #동안성형"""
},
{
"id": "EwgtJUH46dc",
"title": "내시경 눈썹거상술, 낮은 눈썹과 이마를 젊게 | 제이미 성형외과 정기호 원장",
"description": """낮은 이마와 눈썹 때문에 고민하시는 젊은 층을 위한 내시경 눈썹거상술에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 눈썹 위치가 낮아 답답한 인상인 분
• 이마가 좁아 보이는 것이 고민인 분
• 눈썹과 눈 사이 거리가 좁은 분
• 이마거상술보다 가벼운 수술을 원하시는 분
📌 내시경 눈썹거상술이란?
두피 내 절개를 통해 눈썹을 이상적인 위치로 올려주는 수술입니다. 이마거상술보다 회복이 빠르고, 젊은 층의 눈썹 라인 교정에 적합합니다.
📌 제이미 내시경 눈썹거상술 특징
• 눈썹을 이상적인 위치로 리프팅
• 이마 라인 개선 효과
• 이마거상술 대비 짧은 회복 기간
• 자연스러운 눈썹 아치 형성
⏱️ 챕터
0:00 인트로
0:15 내시경 눈썹거상술이란?
0:50 이마거상술과의 차이
1:30 수술 방법 설명
2:15 회복 과정 안내
3:00 적합한 대상
3:35 마무리
#내시경눈썹거상술 #눈썹거상술 #눈썹리프팅 #이마성형 #동안성형"""
},
{
"id": "gfbJlqlAIfg",
"title": "동안 성형, 젊고 생기 있는 인상을 만드는 방법 | 제이미 성형외과 정기호 원장",
"description": """처지고 볼륨이 빠진 얼굴을 젊고 생기 있게 개선하는 동안 성형에 대해 정기호 원장이 설명해 드립니다.
📌 제이미 동안 성형 종류
• 앞광대 리프팅 - 눈밑부터 팔자주름까지 한 번에
• 스마스 리프팅 - 표정근막층부터 근본적인 안면거상
• 자가 지방이식 - 반영구적으로 유지되는 자연스러운 볼륨
• 실 리프팅 - 절개 없이 처진 피부를 당기는 방법
• 하이푸 리프팅 - 회복 기간 없이 피부 탄력 개선
📌 이런 분들께 추천드립니다
• 얼굴 전체적인 처짐이 고민인 분
• 팔자주름이 깊어지신 분
• 볼륨이 빠져 나이 들어 보이는 분
• 피부 탄력이 떨어진 분
⏱️ 챕터
0:00 인트로
0:10 동안 성형의 종류
0:40 수술적 방법 vs 비수술적 방법
1:15 개인별 맞춤 상담의 중요성
1:40 마무리
#동안성형 #리프팅 #지방이식 #얼굴처짐 #안면거상"""
},
{
"id": "lRtAatuhcC4",
"title": "동안 시술, 수술 없이 젊어지는 방법 | 제이미 성형외과 정기호 원장",
"description": """수술 없이 보톡스, 필러, 실 리프팅 등으로 젊어지는 동안 시술에 대해 정기호 원장이 설명해 드립니다.
📌 제이미 동안 시술 종류
보톡스
• 이마 주름, 미간 주름, 눈가 주름
• 사각턱 축소
• 효과 지속 기간 약 4개월
필러
• 코 높이 교정
• 볼륨 보충 (팔자, 볼, 턱)
• 효과 지속 기간 6개월-2년 (제품별 상이)
실 리프팅
• 절개 없이 처진 피부를 당기는 시술
• 콜라겐 생성 촉진
• 효과 지속 기간 약 1년
📌 이런 분들께 추천드립니다
• 수술 없이 빠른 개선을 원하시는 분
• 특정 부위만 보완하고 싶으신 분
• 시술 후 바로 일상 복귀가 필요하신 분
• 수술 전 미리 효과를 확인해보고 싶으신 분
📌 솔직한 조언
"쁘띠 시술은 유지 관리가 필요합니다. 반영구적 효과를 원하시면 수술적 방법을 고려해보시기를 권장드립니다."
⏱️ 챕터
0:00 인트로
0:15 보톡스 시술 설명
0:45 필러 시술 설명
1:15 실 리프팅 설명
1:50 시술별 효과 및 지속 기간
2:10 마무리
#동안시술 #쁘띠성형 #보톡스 #필러 #실리프팅"""
},
{
"id": "7saghBp2a_A",
"title": "앞광대 리프팅, 눈밑부터 팔자주름까지 한 번에 | 제이미 성형외과 정기호 원장",
"description": """눈밑 꺼짐부터 팔자주름까지 중안면을 한 번에 개선하는 앞광대 리프팅에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 눈밑이 꺼지고 다크서클이 심한 분
• 팔자주름이 깊어지신 분
• 앞광대가 꺼져 볼륨이 부족한 분
• 중안면 전체적인 처짐이 고민인 분
📌 앞광대 리프팅이란?
눈밑부터 앞광대, 팔자주름까지 중안면 전체를 리프팅하여 젊고 생기 있는 인상을 만드는 수술입니다.
📌 제이미 앞광대 리프팅 특징
• 눈밑 꺼짐, 앞광대, 팔자주름 동시 개선
• 자연스러운 볼륨 회복
• 얼굴 전체 조화를 고려한 수술 계획
⏱️ 챕터
0:00 인트로
0:10 앞광대 리프팅이란?
0:35 개선 가능한 부위 설명
1:00 수술 방법 안내
1:25 회복 과정 및 효과 지속 기간
#앞광대리프팅 #중안면리프팅 #팔자주름 #동안성형 #리프팅"""
},
{
"id": "Mq6zcx_8owY",
"title": "스마스 리프팅, 표정근막층부터 근본적인 안면거상 | 제이미 성형외과 정기호 원장",
"description": """피부 아래 근막층(SMAS)부터 근본적으로 리프팅하는 스마스 리프팅에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 얼굴 전체적인 처짐이 고민인 분
• 실리프팅이나 하이푸로는 효과가 부족하신 분
• 오래 지속되는 리프팅을 원하시는 분
• 턱선과 목선까지 개선하고 싶으신 분
📌 스마스 리프팅이란?
SMAS(Superficial Musculo-Aponeurotic System)는 피부 아래 근막층으로, 이 층을 함께 당겨주어야 자연스럽고 오래 지속되는 리프팅 효과를 얻을 수 있습니다.
📌 제이미 스마스 리프팅 특징
• 표정 근막층부터 근본적인 리프팅
• 5년 이상 효과 지속
• 턱선, 목선까지 자연스러운 개선
• 개인별 처짐 정도에 맞춘 맞춤 수술
⏱️ 챕터
0:00 인트로
0:10 스마스(SMAS)층이란?
0:35 스마스 리프팅의 원리
1:00 수술 방법 설명
1:30 회복 과정 및 효과 지속 기간
#스마스리프팅 #안면거상술 #SMAS리프팅 #얼굴리프팅 #동안성형"""
},
{
"id": "_bCJDZx2L2I",
"title": "자가 지방이식, 반영구적으로 유지되는 자연스러운 볼륨 | 제이미 성형외과 정기호 원장",
"description": """본인의 지방을 이식하여 반영구적으로 유지되는 자연스러운 볼륨을 만드는 자가 지방이식에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 이마, 볼, 눈밑 등 볼륨이 부족한 분
• 필러보다 오래 지속되는 효과를 원하시는 분
• 자연스러운 볼륨감을 원하시는 분
• 얼굴 전체적인 균형을 맞추고 싶으신 분
📌 자가 지방이식이란?
"나무 옮겨 심는 거랑 똑같다고 하거든요. 한 번 옮겨 심은 나무는 그 자리에서 계속 자라는 거예요."
본인의 복부, 허벅지 등에서 채취한 지방을 정제하여 볼륨이 필요한 부위에 이식하는 시술입니다.
📌 제이미 자가 지방이식 특징
• 생착률 30-40% 고려한 충분한 이식량
• 자연스러운 볼륨 형성
• 반영구적 효과 지속
• 이마, 볼, 눈밑, 팔자 등 다양한 부위 적용
⏱️ 챕터
0:00 인트로
0:10 자가 지방이식이란?
0:35 지방 생착의 원리
1:00 시술 과정 설명
1:30 회복 기간 및 관리 방법
#자가지방이식 #지방이식 #볼륨성형 #이마지방이식 #동안성형"""
},
{
"id": "kXbP1T6ICxY",
"title": "하이푸 리프팅, 회복 기간 없이 피부 탄력 개선 | 제이미 성형외과 정기호 원장",
"description": """절개나 주사 없이 초음파로 피부 탄력을 개선하는 하이푸 리프팅에 대해 정기호 원장이 설명해 드립니다.
📌 이런 분들께 추천드립니다
• 시술 후 바로 일상 복귀가 필요하신 분
• 절개나 주사가 부담스러우신 분
• 피부 탄력이 떨어진 초기 노화 단계인 분
• 정기적인 피부 관리를 원하시는 분
📌 하이푸(HIFU) 리프팅이란?
고강도 집속 초음파(High Intensity Focused Ultrasound)를 이용해 피부 깊은 층(SMAS층)에 열 자극을 주어 콜라겐 재생을 촉진하는 시술입니다.
📌 하이푸 리프팅 특징
• 절개, 주사 없는 비침습 시술
• 시술 직후 일상생활 가능
• 시술 시간 30분-1시간
• 효과 지속 기간 3-6개월
📌 솔직한 조언
"하이푸는 유지 관리 개념의 시술입니다. 이미 처짐이 진행된 경우에는 수술적 리프팅이 더 효과적일 수 있습니다."
⏱️ 챕터
0:00 인트로
0:10 하이푸(HIFU)란?
0:35 작용 원리 설명
1:00 시술 과정 안내
1:30 효과 및 유지 기간
#하이푸 #하이푸리프팅 #울쎄라 #리프팅시술 #동안시술"""
}
]
def get_authenticated_service():
"""Authenticate and return YouTube API service."""
creds = None
if TOKEN_FILE.exists():
with open(TOKEN_FILE, 'rb') as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
if not CLIENT_SECRETS_FILE.exists():
print(f"[ERROR] OAuth client secret file not found: {CLIENT_SECRETS_FILE}")
return None
flow = InstalledAppFlow.from_client_secrets_file(
CLIENT_SECRETS_FILE, SCOPES
)
creds = flow.run_local_server(port=8080)
with open(TOKEN_FILE, 'wb') as token:
pickle.dump(creds, token)
return build('youtube', 'v3', credentials=creds)
def update_video_metadata(youtube, video_id, title, description, dry_run=True):
"""Update video title and description."""
full_description = description + "\n\n" + COMMON_FOOTER
if dry_run:
print(f"\n[DRY-RUN] Would update video: {video_id}")
print(f" Title: {title[:50]}...")
print(f" Description length: {len(full_description)} chars")
return True
try:
# First, get current video info to preserve other metadata
video_response = youtube.videos().list(
part="snippet,status",
id=video_id
).execute()
if not video_response.get('items'):
print(f"[ERROR] Video not found: {video_id}")
return False
video = video_response['items'][0]
snippet = video['snippet']
# Update snippet with new title and description
snippet['title'] = title
snippet['description'] = full_description
# Execute update
youtube.videos().update(
part="snippet",
body={
"id": video_id,
"snippet": snippet
}
).execute()
print(f"✅ Updated: {video_id} - {title[:40]}...")
return True
except Exception as e:
print(f"❌ Failed to update {video_id}: {e}")
return False
def main():
import argparse
parser = argparse.ArgumentParser(description='Update Jamie YouTube video metadata')
parser.add_argument('--dry-run', action='store_true', default=True,
help='Preview changes without actually updating (default: True)')
parser.add_argument('--execute', action='store_true',
help='Actually execute the updates')
parser.add_argument('--video-id', type=str,
help='Update only a specific video ID')
args = parser.parse_args()
dry_run = not args.execute
print("=" * 60)
print("Jamie Clinic - YouTube Batch Metadata Update")
print("=" * 60)
print(f"Mode: {'DRY-RUN (preview only)' if dry_run else 'EXECUTE (will update videos)'}")
print(f"Videos to process: {len(VIDEOS)}")
print("=" * 60)
if not dry_run:
confirm = input("\n⚠️ This will update video metadata. Type 'yes' to confirm: ")
if confirm.lower() != 'yes':
print("Cancelled.")
return
youtube = get_authenticated_service()
if not youtube:
sys.exit(1)
success_count = 0
fail_count = 0
for video in VIDEOS:
if args.video_id and video['id'] != args.video_id:
continue
result = update_video_metadata(
youtube,
video['id'],
video['title'],
video['description'],
dry_run=dry_run
)
if result:
success_count += 1
else:
fail_count += 1
print("\n" + "=" * 60)
print("Summary")
print("=" * 60)
print(f"{'Would update' if dry_run else 'Updated'}: {success_count} videos")
if fail_count > 0:
print(f"Failed: {fail_count} videos")
if dry_run:
print("\n💡 To execute actual updates, run with --execute flag:")
print(" python jamie_youtube_batch_update.py --execute")
if __name__ == "__main__":
main()

View File

@@ -1,16 +1,58 @@
---
name: jamie-youtube-manager
description: |
"YouTube Channel Manager and SEO Auditor for Jamie Plastic Surgery Clinic (제이미성형외과). Audits videos, Shorts, and playlists for SEO optimization including: metadata quality (title, description, tags), chapter timestamps, transcript accuracy, VideoObject schema validation, and internationalization settings. Evaluates video/playlist URLs and provides detailed checklists with enhancement recommendations. Use when auditing Jamie's YouTube content - triggers: "YouTube 감사", "유튜브 SEO", "video audit", "playlist check", "영상 최적화", "챕터 확인", "자막 검토". For content script writing, use jamie-brand-editor.
license: Internal-use Only"
YouTube Channel Manager and SEO Auditor for Jamie Plastic Surgery Clinic (제이미성형외과). Includes CLI scripts for API-based channel/video management and SEO audit capabilities. Check video status, get channel statistics, fetch video info from URLs, batch update metadata, and audit videos for SEO optimization (title, description, tags, timestamps, schema). Use when working with Jamie YouTube content - triggers: "YouTube 감사", "유튜브 SEO", "video audit", "check video", "channel status", "영상 최적화", "챕터 확인".
allowed-tools: Read, Glob, Grep, Write, Edit, Bash, WebFetch
license: Internal-use Only
---
# Jamie YouTube Manager Skill
> **Purpose**: YouTube Channel SEO Auditor & Content Manager for Jamie Plastic Surgery Clinic
> **Platform**: Claude Desktop
> **Platform**: Claude Code (CLI) + Claude Desktop
> **Input**: YouTube video URLs, playlist URLs, or channel data
> **Output**: Detailed audit checklist + SEO enhancement recommendations
> **Output**: Video info, channel stats, audit checklist + SEO recommendations
---
## CLI Scripts (Claude Code)
### Setup
```bash
cd ~/Project/claude-skills-factory/custom-skills/43-jamie-youtube-manager/code/scripts
source venv/bin/activate
```
### Available Scripts
| Script | Purpose | Usage |
|--------|---------|-------|
| `jamie_channel_status.py` | Channel stats overview | `python jamie_channel_status.py` |
| `jamie_video_info.py` | Video details from URL | `python jamie_video_info.py "URL"` |
| `jamie_youtube_api_test.py` | API connectivity test | `python jamie_youtube_api_test.py` |
| `jamie_youtube_batch_update.py` | Batch metadata update | `python jamie_youtube_batch_update.py` |
### Channel Stats Example
```bash
python jamie_channel_status.py
# Output: Channel name, subscribers, views, recent videos with status
```
### Video Info from URL
```bash
python jamie_video_info.py "https://youtu.be/VIDEO_ID"
# Output: Title, description, duration, views, likes, tags, timestamps, privacy status
```
### Integration with Notion Writer
```bash
# Save video info to Notion
python jamie_video_info.py "URL" > ../output/video_status.md
cd ~/Project/claude-skills-factory/custom-skills/02-notion-writer/code/scripts
source venv/bin/activate
python notion_writer.py -p NOTION_PAGE_URL -f ../../43-jamie-youtube-manager/code/output/video_status.md
```
---