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