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

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

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

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

View File

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

View File

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

View File

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

View File

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