Files
our-claude-skills/custom-skills/43-jamie-youtube-manager/code/scripts/jamie_video_info.py
Andrew Yim c6ab33726f 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>
2025-12-26 19:37:19 +09:00

232 lines
7.3 KiB
Python

#!/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()