From c6ab33726f9b5fde1016845b325e97d32a2cae6e Mon Sep 17 00:00:00 2001 From: Andrew Yim Date: Fri, 26 Dec 2025 19:31:43 +0900 Subject: [PATCH] feat(skills): Add notion-writer skill and YouTube manager CLI scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .claude/settings.local.json | 10 +- custom-skills/02-notion-writer/code/CLAUDE.md | 233 ++++++ .../code/scripts/.env.example | 4 + .../code/scripts/notion_writer.py | 594 +++++++++++++++ .../02-notion-writer/desktop/SKILL.md | 86 +++ .../43-jamie-youtube-manager/code/CLAUDE.md | 191 ++++- .../code/scripts/jamie_channel_status.py | 169 +++++ .../code/scripts/jamie_video_info.py | 231 ++++++ .../code/scripts/jamie_youtube_api_test.py | 158 ++++ .../scripts/jamie_youtube_batch_update.py | 705 ++++++++++++++++++ .../43-jamie-youtube-manager/desktop/SKILL.md | 50 +- 11 files changed, 2424 insertions(+), 7 deletions(-) create mode 100644 custom-skills/02-notion-writer/code/CLAUDE.md create mode 100644 custom-skills/02-notion-writer/code/scripts/.env.example create mode 100644 custom-skills/02-notion-writer/code/scripts/notion_writer.py create mode 100644 custom-skills/02-notion-writer/desktop/SKILL.md create mode 100644 custom-skills/43-jamie-youtube-manager/code/scripts/jamie_channel_status.py create mode 100644 custom-skills/43-jamie-youtube-manager/code/scripts/jamie_video_info.py create mode 100644 custom-skills/43-jamie-youtube-manager/code/scripts/jamie_youtube_api_test.py create mode 100644 custom-skills/43-jamie-youtube-manager/code/scripts/jamie_youtube_batch_update.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a32a0f3..a36fee2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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:*)" ] } } diff --git a/custom-skills/02-notion-writer/code/CLAUDE.md b/custom-skills/02-notion-writer/code/CLAUDE.md new file mode 100644 index 0000000..48d103d --- /dev/null +++ b/custom-skills/02-notion-writer/code/CLAUDE.md @@ -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* diff --git a/custom-skills/02-notion-writer/code/scripts/.env.example b/custom-skills/02-notion-writer/code/scripts/.env.example new file mode 100644 index 0000000..f319220 --- /dev/null +++ b/custom-skills/02-notion-writer/code/scripts/.env.example @@ -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 diff --git a/custom-skills/02-notion-writer/code/scripts/notion_writer.py b/custom-skills/02-notion-writer/code/scripts/notion_writer.py new file mode 100644 index 0000000..4df2b78 --- /dev/null +++ b/custom-skills/02-notion-writer/code/scripts/notion_writer.py @@ -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() diff --git a/custom-skills/02-notion-writer/desktop/SKILL.md b/custom-skills/02-notion-writer/desktop/SKILL.md new file mode 100644 index 0000000..0df9820 --- /dev/null +++ b/custom-skills/02-notion-writer/desktop/SKILL.md @@ -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 +``` diff --git a/custom-skills/43-jamie-youtube-manager/code/CLAUDE.md b/custom-skills/43-jamie-youtube-manager/code/CLAUDE.md index 17d22ee..4d99780 100644 --- a/custom-skills/43-jamie-youtube-manager/code/CLAUDE.md +++ b/custom-skills/43-jamie-youtube-manager/code/CLAUDE.md @@ -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= +GOOGLE_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* diff --git a/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_channel_status.py b/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_channel_status.py new file mode 100644 index 0000000..8a552b9 --- /dev/null +++ b/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_channel_status.py @@ -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() diff --git a/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_video_info.py b/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_video_info.py new file mode 100644 index 0000000..165b748 --- /dev/null +++ b/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_video_info.py @@ -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() diff --git a/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_youtube_api_test.py b/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_youtube_api_test.py new file mode 100644 index 0000000..01736b0 --- /dev/null +++ b/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_youtube_api_test.py @@ -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 # Get video info") + print(" python jamie_youtube_batch_update.py --dry-run") + + +if __name__ == "__main__": + main() diff --git a/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_youtube_batch_update.py b/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_youtube_batch_update.py new file mode 100644 index 0000000..659e19a --- /dev/null +++ b/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_youtube_batch_update.py @@ -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() diff --git a/custom-skills/43-jamie-youtube-manager/desktop/SKILL.md b/custom-skills/43-jamie-youtube-manager/desktop/SKILL.md index de03831..41e60a4 100644 --- a/custom-skills/43-jamie-youtube-manager/desktop/SKILL.md +++ b/custom-skills/43-jamie-youtube-manager/desktop/SKILL.md @@ -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 +``` ---