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:
@@ -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*
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user