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()