AgentStack
SKILL verified MIT Self-run

Youtube Title Tag Optimizer

skill-nikhilbhansali-youtube-data-skills-youtube-title-tag-optimizer · by nikhilbhansali

Optimize YouTube video titles, tags, and descriptions before publishing using YouTube Data API v3. Analyze top-ranking videos for a keyword to reverse-engineer winning title patterns, extract effective tags, and generate optimized title variations. Use when users want to (1) Optimize a video title before publishing, (2) Find the best tags for a video, (3) Analyze what title patterns work for a ke…

No reviews yet
0 installs
6 views
0.0% view→install

Install

$ agentstack add skill-nikhilbhansali-youtube-data-skills-youtube-title-tag-optimizer

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Youtube Title Tag Optimizer? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

YouTube Title & Tag Optimizer

Optimize your video title, tags, and description before publishing by analyzing what works for top-ranking videos.

Usage

/youtube-title-tag-optimizer "air fryer recipes"
/youtube-title-tag-optimizer "Python tutorial" --my-title "Learn Python in 10 Minutes"
/youtube-title-tag-optimizer --keyword "home workout" --my-title "Best Home Workout for Beginners 2024"

Instructions

When the user invokes this skill:

Step 1: Parse Arguments

Extract from the user's input:

  • Keyword (required): The topic/keyword to optimize for
  • --my-title "..." (optional): User's working title to score and improve

Step 2: Get API Key

Check the user's Claude memory for a YouTube Data API v3 key. If not found, ask: > "I need a YouTube Data API v3 key to analyze ranking videos. You can get one from the Google Cloud Console. Please paste your key."

Step 3: Write the Script

Write the following Python script to /tmp/_yt_title_optimizer_XXXX.py (where XXXX is a random suffix, e.g. $(openssl rand -hex 4)):

#!/usr/bin/env python3
"""
YouTube Title & Tag Optimizer

Analyzes top-ranking videos for a keyword to extract winning title patterns,
effective tags, and generate optimization recommendations.

Usage:
    YT_API_KEY=KEY python3 /tmp/_yt_title_optimizer_XXXX.py "keyword"
    YT_API_KEY=KEY python3 /tmp/_yt_title_optimizer_XXXX.py "keyword" --my-title "My Title"
"""

import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
from collections import Counter

try:
    from googleapiclient.discovery import build
    from googleapiclient.errors import HttpError
except ImportError:
    print("ERROR: google-api-python-client not installed. Run: pip3 install google-api-python-client")
    sys.exit(1)

def parse_duration(iso_duration: str) -> int:
    m = re.match(r"PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?", iso_duration or "")
    if not m:
        return 0
    return int(m.group(1) or 0) * 3600 + int(m.group(2) or 0) * 60 + int(m.group(3) or 0)

def format_number(n: int) -> str:
    if n >= 1_000_000: return f"{n/1_000_000:.1f}M"
    if n >= 1_000: return f"{n/1_000:.1f}K"
    return str(n)

def analyze_title(title: str) -> dict:
    """Analyze a single title for patterns."""
    analysis = {
        "length_chars": len(title),
        "length_words": len(title.split()),
        "has_number": bool(re.search(r'\d', title)),
        "has_question": title.rstrip().endswith("?"),
        "has_how_to": bool(re.search(r'how\s+to', title, re.I)),
        "has_brackets": bool(re.search(r'[\[\(]', title)),
        "has_parentheses": bool(re.search(r'\(', title)),
        "has_caps_word": bool(re.search(r'\b[A-Z]{2,}\b', title)),
        "has_emoji": bool(re.search(r'[^\w\s,.\-!?\'\"()\[\]:;/\\@#$%^&*+=~`|<>]', title)),
        "has_year": bool(re.search(r'20[12]\d', title)),
        "has_list_format": bool(re.search(r'^\d+\s', title)),
        "has_vs": bool(re.search(r'\bvs\.?\b', title, re.I)),
        "has_colon": ":" in title,
        "has_pipe": "|" in title,
        "has_dash_separator": " - " in title or " — " in title,
        "has_exclamation": "!" in title,
        "starts_with_how": title.lower().startswith("how"),
        "starts_with_why": title.lower().startswith("why"),
        "starts_with_what": title.lower().startswith("what"),
        "starts_with_number": bool(re.match(r'^\d', title)),
    }

    # Detect power words
    power_words = {
        "urgency": ["now", "today", "immediately", "urgent", "hurry", "fast", "quick", "instantly", "stop"],
        "curiosity": ["secret", "hidden", "surprising", "shocking", "unexpected", "weird", "strange", "mystery", "truth", "reveal"],
        "value": ["free", "best", "top", "ultimate", "complete", "guide", "hack", "tips", "tricks", "easy", "simple"],
        "emotional": ["amazing", "incredible", "insane", "mind-blowing", "life-changing", "game-changer", "beautiful", "perfect", "love", "hate"],
        "fear": ["mistake", "wrong", "avoid", "never", "worst", "dangerous", "warning", "risk", "fail", "scam", "don't"],
    }

    title_lower = title.lower()
    found_power_words = {}
    for category, words in power_words.items():
        matches = [w for w in words if w in title_lower]
        if matches:
            found_power_words[category] = matches
    analysis["power_words"] = found_power_words

    # Detect title structure/formula
    structures = []
    if re.match(r'^\d+\s', title):
        structures.append("listicle")
    if re.search(r'how\s+to', title, re.I):
        structures.append("how-to")
    if title.rstrip().endswith("?"):
        structures.append("question")
    if re.search(r'\bvs\.?\b', title, re.I):
        structures.append("comparison")
    if re.search(r'\breview\b', title, re.I):
        structures.append("review")
    if re.search(r'in\s+\d+\s+(minute|min|second|sec|hour|day|step)', title, re.I):
        structures.append("time-bound")
    if re.search(r'for\s+(beginners|newbies|noobs|starters)', title, re.I):
        structures.append("beginner-targeted")
    if re.search(r'(complete|ultimate|full|definitive)\s+guide', title, re.I):
        structures.append("comprehensive-guide")
    if re.search(r'\b(do|don\'t|never|stop|avoid)\b', title, re.I):
        structures.append("imperative")
    analysis["structures"] = structures

    return analysis

def main():
    parser = argparse.ArgumentParser(description="Optimize YouTube titles and tags")
    parser.add_argument("keyword", help="Keyword to optimize for")
    parser.add_argument("--my-title", default=None, help="Your working title to score")
    parser.add_argument("--output-dir", default=None, help="Output directory")
    args = parser.parse_args()

    api_key = os.environ.get("YT_API_KEY")
    if not api_key:
        print("ERROR: YT_API_KEY environment variable not set.")
        sys.exit(1)

    youtube = build("youtube", "v3", developerKey=api_key)
    quota_used = 0

    print(f"Analyzing keyword: {args.keyword}")
    if args.my_title:
        print(f"Your title: {args.my_title}\n")

    # --- Search by relevance ---
    print("Fetching top-ranking videos (by relevance)...")
    try:
        relevance_resp = youtube.search().list(
            part="snippet", q=args.keyword, type="video",
            order="relevance", maxResults=50,
        ).execute()
        relevance_ids = [item["id"]["videoId"] for item in relevance_resp.get("items", [])]
        quota_used += 100
    except HttpError as e:
        print(f"Search error: {e}")
        sys.exit(1)

    # --- Search by view count ---
    print("Fetching most-viewed videos...")
    try:
        viewcount_resp = youtube.search().list(
            part="snippet", q=args.keyword, type="video",
            order="viewCount", maxResults=50,
        ).execute()
        viewcount_ids = [item["id"]["videoId"] for item in viewcount_resp.get("items", [])]
        quota_used += 100
    except HttpError as e:
        viewcount_ids = []
        print(f"View count search error: {e}")

    # Combine and deduplicate
    all_ids = list(dict.fromkeys(relevance_ids + viewcount_ids))
    print(f"Total unique videos: {len(all_ids)}")

    # --- Fetch video details ---
    print("Fetching video details...")
    videos = []
    for i in range(0, len(all_ids), 50):
        batch = all_ids[i:i+50]
        try:
            resp = youtube.videos().list(
                part="snippet,statistics,contentDetails",
                id=",".join(batch),
            ).execute()
            videos.extend(resp.get("items", []))
            quota_used += 1
        except HttpError as e:
            print(f"Error: {e}")
    print(f"  Got details for {len(videos)} videos")

    # --- Process videos ---
    processed = []
    all_tags = Counter()
    title_analyses = []

    for v in videos:
        views = int(v.get("statistics", {}).get("viewCount", 0))
        likes = int(v.get("statistics", {}).get("likeCount", 0))
        comments = int(v.get("statistics", {}).get("commentCount", 0))
        duration = parse_duration(v.get("contentDetails", {}).get("duration", ""))
        title = v["snippet"]["title"]
        tags = v.get("snippet", {}).get("tags", [])

        for tag in tags:
            all_tags[tag.lower()] += 1

        t_analysis = analyze_title(title)
        title_analyses.append(t_analysis)

        processed.append({
            "video_id": v["id"],
            "title": title,
            "title_analysis": t_analysis,
            "channel": v["snippet"]["channelTitle"],
            "views": views,
            "likes": likes,
            "comments": comments,
            "engagement_rate": round((likes + comments) / max(views, 1) * 100, 2),
            "duration_sec": duration,
            "tags": tags,
            "tag_count": len(tags),
            "description_first_line": v["snippet"].get("description", "").split("\n")[0][:200],
            "published_at": v["snippet"]["publishedAt"],
        })

    processed.sort(key=lambda x: x["views"], reverse=True)

    # --- Aggregate title analysis ---
    total = len(title_analyses) or 1
    aggregate_title = {
        "avg_length_chars": round(sum(t["length_chars"] for t in title_analyses) / total, 1),
        "avg_length_words": round(sum(t["length_words"] for t in title_analyses) / total, 1),
        "pct_has_number": round(sum(1 for t in title_analyses if t["has_number"]) / total * 100, 1),
        "pct_question": round(sum(1 for t in title_analyses if t["has_question"]) / total * 100, 1),
        "pct_how_to": round(sum(1 for t in title_analyses if t["has_how_to"]) / total * 100, 1),
        "pct_brackets": round(sum(1 for t in title_analyses if t["has_brackets"]) / total * 100, 1),
        "pct_caps_word": round(sum(1 for t in title_analyses if t["has_caps_word"]) / total * 100, 1),
        "pct_emoji": round(sum(1 for t in title_analyses if t["has_emoji"]) / total * 100, 1),
        "pct_year": round(sum(1 for t in title_analyses if t["has_year"]) / total * 100, 1),
        "pct_list_format": round(sum(1 for t in title_analyses if t["has_list_format"]) / total * 100, 1),
        "pct_colon": round(sum(1 for t in title_analyses if t["has_colon"]) / total * 100, 1),
        "pct_pipe": round(sum(1 for t in title_analyses if t["has_pipe"]) / total * 100, 1),
        "pct_exclamation": round(sum(1 for t in title_analyses if t["has_exclamation"]) / total * 100, 1),
    }

    structure_counter = Counter()
    for t in title_analyses:
        for s in t["structures"]:
            structure_counter[s] += 1
    aggregate_title["structures"] = {k: round(v/total*100, 1) for k, v in structure_counter.most_common()}

    power_category_counter = Counter()
    for t in title_analyses:
        for cat in t["power_words"]:
            power_category_counter[cat] += 1
    aggregate_title["power_word_categories"] = {k: round(v/total*100, 1) for k, v in power_category_counter.most_common()}

    word_counter = Counter()
    stop_words = {"the", "a", "an", "is", "it", "in", "on", "at", "to", "for", "of", "and", "or", "but", "with", "you", "your", "my", "this", "that", "i", "me", "we", "how", "what", "why", "do", "does", "can", "will", "be", "are", "was", "not", "no", "so", "if"}
    for v in processed:
        words = re.findall(r'[a-zA-Z]{3,}', v["title"].lower())
        for w in words:
            if w not in stop_words:
                word_counter[w] += 1
    aggregate_title["common_title_words"] = word_counter.most_common(30)

    # --- Score user's title if provided ---
    my_title_score = None
    if args.my_title:
        my_analysis = analyze_title(args.my_title)
        score = 0
        feedback = []

        if 40  80:
            feedback.append("Title may be too long -- could get truncated in search results")
        else:
            score += 10
            feedback.append("Title length is acceptable but could be optimized")

        keyword_lower = args.keyword.lower()
        title_lower = args.my_title.lower()
        if keyword_lower in title_lower:
            score += 20
            if title_lower.startswith(keyword_lower) or title_lower[:20].find(keyword_lower) >= 0:
                score += 5
                feedback.append("Keyword appears early in title -- great for SEO")
            else:
                feedback.append("Keyword is present in title")
        else:
            kw_words = keyword_lower.split()
            matches = sum(1 for w in kw_words if w in title_lower)
            if matches > 0:
                score += 10
                feedback.append(f"Partial keyword match ({matches}/{len(kw_words)} words)")
            else:
                feedback.append("WARNING: Keyword not found in title -- critical for SEO")

        if my_analysis["has_number"]:
            score += 10
            feedback.append("Contains a number -- increases click-through rate")

        if my_analysis["power_words"]:
            score += 10
            cats = list(my_analysis["power_words"].keys())
            feedback.append(f"Uses power words ({', '.join(cats)})")
        else:
            feedback.append("Consider adding power words for emotional impact")

        if my_analysis["structures"]:
            score += 10
            feedback.append(f"Uses proven structure: {', '.join(my_analysis['structures'])}")
        else:
            feedback.append("Consider using a proven structure (how-to, listicle, question)")

        if my_analysis["has_brackets"]:
            score += 5
            feedback.append("Uses brackets -- can boost CTR by 33%")

        if my_analysis["has_caps_word"]:
            score += 5
            feedback.append("Strategic use of CAPS for emphasis")

        top_10_titles = [v["title"] for v in processed[:10]]
        common_patterns_in_top = set()
        for t in top_10_titles:
            ta = analyze_title(t)
            for s in ta["structures"]:
                common_patterns_in_top.add(s)

        matching_patterns = set(my_analysis["structures"]) & common_patterns_in_top
        if matching_patterns:
            score += 10
            feedback.append(f"Matches top-performer patterns: {', '.join(matching_patterns)}")

        my_title_score = {
            "title": args.my_title,
            "score": min(score, 100),
            "analysis": my_analysis,
            "feedback": feedback,
        }

    # --- Build output ---
    output = {
        "keyword": args.keyword,
        "analyzed_at": datetime.now(timezone.utc).isoformat(),
        "total_videos_analyzed": len(processed),
        "aggregate_title_analysis": aggregate_title,
        "top_tags": all_tags.most_common(50),
        "tag_stats": {
            "unique_tags": len(all_tags),
            "avg_tags_per_video": round(sum(v["tag_count"] for v in processed) / len(processed), 1) if processed else 0,
        },
        "top_videos": [
            {
                "title": v["title"],
                "video_id": v["video_id"],
                "views": v["views"],
                "engagement_rate": v["engagement_rate"],
                "channel": v["channel"],
                "tag_count": v["tag_count"],
                "title_analysis": v["title_analysis"],
                "description_first_line": v["description_first_line"],
            }
            for v in processed[:20]
        ],
        "all_videos": processed,
        "my_title_score": my_title_score,
        "quota_used": {
            "total_estimated": quota_used,
        },
    }

    # --- Save ---
    safe_kw = re.sub(r'[^a-zA-Z0-9_-]', '_', args.keyword)[:100]
    date_str = datetime.now().strftime("%Y%m%d")
    output_dir = args.output_dir or f"yt_optimize_{safe_kw}_{date_str}"
    os.makedirs(output_dir, exist_ok=True)

    output_file = os.path.join(output_dir, "title_tag_data.json")
    try:
        with open(output_file, "w", encoding="utf-8") as f:
            json.dump(output, f, indent=2, ensure_ascii=False)
    except (IOErr

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [nikhilbhansali](https://github.com/nikhilbhansali)
- **Source:** [nikhilbhansali/youtube-data-skills](https://github.com/nikhilbhansali/youtube-data-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.