Skill: mjohannp/youtube-ai-videos (version 1.0.1)

--- name: youtube-ai-videos description: Fetch latest AI-related YouTube videos from curated channels using YouTube Data API v3 and filter by keywords requiresEnvVars: - YOUTUBE_API_KEY --- # YouTube AI Videos Fetch and display the latest AI-related videos from YouTube channels using YouTube Data API v3. ## Requirements **This skill requires a YouTube Data API v3 key to function.** The API key can be provided in three ways (priority order): 1. **Environment variable:** `YOUTUBE_API_KEY` 2. **Secrets file:** `~/.openclaw/secrets/youtube_api_key.txt` 3. **Config file:** `config.json` (fallback, not recommended for security) Get a free API key: https://console.cloud.google.com/apis/api/youtube.googleapis.com ## Usage Use this skill when you need recent AI-related videos from YouTube. ## Workflow 1. Fetch recent videos from configured YouTube channels using YouTube Data API v3 2. Filter videos by keywords in title 3. Filter by max age (default: 3 days) 4. Return up to 15 videos (configurable) 5. Sort by publication date (newest first) ## Configuration All settings are in `config.json`: - **channels:** List of YouTube channel handles (@handle) or IDs - **keywords:** List of keywords to search for - **maxVideos:** Maximum number of videos to return (default: 15) - **maxAgeDays:** Maximum video age in days (default: 3) - **youtubeApiKey:** Fallback YouTube Data API v3 key (use environment/secrets instead) ## Adding Channels Find YouTube channel handles or IDs: 1. Go to channel page 2. Look at URL: `youtube.com/@CHANNELNAME` or `youtube.com/channel/CHANNEL_ID` 3. Use @handle format (recommended) or channel ID ## Output Format Each video includes: - Number (1-15) - Time ago (e.g., "2h ago") - Title with keyword matches highlighted in bold - Channel name - Direct YouTube link Example: ``` 1. [2h ago] [OpenClaw: The Next Generation of AI Agents](https://youtube.com/watch?v=...) by @IchBinFabian ``` ## Running the Script ```bash ./scripts/fetch_youtube_ai_videos.py ``` The script loads --> YouTube API key from environment, secrets file, or config (in that order) and outputs filtered videos. ## Security Note For security, prefer storing your API key in: - Environment variable: `export YOUTUBE_API_KEY="your_key"` - Secrets file: `~/.openclaw/secrets/youtube_api_key.txt` Avoid storing the API key directly in `config.json` as it's visible in plain text.

path: README.md
content: # YouTube AI Videos 🎬

Fetch latest AI-related videos from YouTube channels with keyword filtering using YouTube Data API v3.

![Skill Version](https://img.shields.io/badge/version-1.0.1-blue)
![License](https://img.shields.io/badge/license-MIT-green)
![Python](https://img.shields.io/badge/python-3.8+-yellow)

## ⚠️ Requirement

**This skill requires a YouTube Data API v3 key to function.** The YouTube RSS feeds have been disabled, so this skill exclusively uses the YouTube Data API v3.

## ✨ Features

- 🔍 **Keyword Filtering** — Only videos matching your keywords
- 📅 **Age Filter** — Configurable maximum video age (default: 3 days)
- 🎯 **Channel Selection** — Curated AI-focused YouTube channels
- 🔐 **Secure API Key** — Multiple storage options (env var, secrets file)
- 🏷️ **Keyword Highlighting** — Matches are highlighted in output
- 📊 **Configurable** — Channels, keywords, max videos, age limits
- 🚀 **Fast** — Uses YouTube Data API v3 for efficient fetching

## 📋 What it Does

This skill fetches recent videos from your favorite AI-focused YouTube channels using the YouTube Data API v3 and filters them by keywords like "OpenClaw", "LLM", "Agent", "Claude Code", etc.

Perfect for staying up-to-date with the latest AI developments, agent tools, and coding assistants!

## 🚀 Installation

### Option 1: Install via ClawHub (Recommended)

```bash
clawhub install youtube-ai-videos
```

### Option 2: Manual Installation

1. Clone or download this skill to your OpenClaw workspace:
```bash
git clone https://github.com/your-username/youtube-ai-videos.git ~/.openclaw/workspace/youtube-ai-videos
```

2. Ensure Python 3.8+ is installed:
```bash
python3 --version
```

## ⚙️ Configuration

### Step 1: Get YouTube Data API Key (REQUIRED!)

This skill **requires** a YouTube Data API v3 key. Without it, the skill will not work.

1. Go to [Google Cloud Console](https://console.cloud.google.com)
2. Create a new project or use existing
3. Navigate to "APIs & Services" → "Library"
4. Search for "YouTube Data API v3" and enable it
5. Go to "APIs & Services" → "Credentials"
6. Create "API Key"
7. **Important:** Restrict key to "YouTube Data API v3" only

### Step 2: Configure API Key (Secure Options)

Choose one of these methods (ordered by priority):

#### Option A: Secrets File (Recommended ✅)

Store your API key securely:

```bash
echo "YOUR_YOUTUBE_API_KEY" > ~/.openclaw/secrets/youtube_api_key.txt
chmod 600 ~/.openclaw/secrets/youtube_api_key.txt
```

#### Option B: Environment Variable

```bash
export YOUTUBE_API_KEY="YOUR_YOUTUBE_API_KEY"
```

For permanent usage, add to your shell profile (~/.zshrc or ~/.bashrc):

```bash
echo 'export YOUTUBE_API_KEY="YOUR_YOUTUBE_API_KEY"' >> ~/.zshrc
source ~/.zshrc
```

#### Option C: Config File (Fallback ⚠️)

Edit `config.json` and replace the placeholder:

```json
{
  "youtubeApiKey": "YOUR_YOUTUBE_API_KEY"
}
```

**⚠️ Security Warning:** The API key in `config.json` is visible in plain text. Use options A or B instead!

### Step 3: Configure Channels and Keywords

Edit `config.json` to customize:

```json
{
  "channels": [
    "@IchBinFabian",
    "@EverlastAI",
    "@BetterStack",
    "@ChristophMagnussen"
  ],
  "keywords": [
    "OpenClaw",
    "LLM",
    "Agent",
    "Claude Code",
    "RAG"
  ],
  "maxVideos": 15,
  "maxAgeDays": 3
}
```

**Settings Explained:**

| Setting | Description | Default |
|---------|-------------|----------|
| `channels` | YouTube channel handles (@handle) or IDs | 13 AI channels |
| `keywords` | Keywords to filter videos by | AI/LLM related |
| `maxVideos` | Maximum number of videos to return | 15 |
| `maxAgeDays` | Maximum age of videos in days | 3 |
| `youtubeApiKey` | Fallback API key (use secrets instead) | `YOUR_API_KEY_HERE` |

## 🎯 Usage

### Command Line

Run the fetcher:

```bash
~/.openclaw/workspace/youtube-ai-videos/scripts/fetch_youtube_ai_videos.py
```

Or use the alias (if configured):

```bash
youtube-ai
```

### Output Format

```
🎬 Fetching AI videos (last 3 days)
Keywords: openclaw, llm, agent, claude code, opencode, rag
Channels: 13

Fetching from @IchBinFabian...
Fetching from @EverlastAI...
...

1. [7m ago] [Codex App Live: KI-**Agent**en direkt auf deinen Code ansetzen!](https://www.youtube.com/watch?v=...)
   by @IchBinFabian

2. [1h ago] [An **LLM** in Just 200 Lines of Python?! (microGPT)](https://www.youtube.com/watch?v=...)
   by @Better Stack

✅ Found 15 videos (from 13 channels)
```

## 🔧 Advanced Configuration

### Adding More Channels

Find YouTube channel handle or ID:

1. Go to channel page
2. Look at URL: `youtube.com/@CHANNELNAME` or `youtube.com/channel/CHANNEL_ID`
3. Add to `config.json`:
   - Handle format: `"@ChannelName"`
   - ID format: `"UC..."`
   - URL format: `"https://www.youtube.com/CHANNELNAME"`

### Custom Keywords

Add any keywords you want to track:

```json
"keywords": [
  "OpenClaw",
  "LLM",
  "GPT",
  "Claude",
  "AI Coding",
  "RAG",
  "Vector Database"
]
```

### Adjust Time Range

Change the maximum age of videos:

```json
"maxAgeDays": 7  // Show videos from last week
```

Or show only today's videos:

```json
"maxAgeDays": 1  // Show only today's videos
```

## 📊 API Quota

The YouTube Data API v3 free tier includes:

- **10,000 quota units per day**
- **Channel list API** costs 1 unit per request
- **Video search API** costs 100 units per request
- **Video details API** costs 1 unit per request

**Typical Usage:**
- Fetching from 13 channels ≈ 13 units
- Fetching video details ≈ 1-3 units
- **Daily usage ≈ 20-50 units** (well within limits!)

## 🛠️ Troubleshooting

### API Key Not Found

```
❌ YouTube Data API key is required!
```

**Solution:** Make sure your API key is set via:
- Secrets file: `~/.openclaw/secrets/youtube_api_key.txt`
- Environment variable: `export YOUTUBE_API_KEY="..."`
- Config file (fallback): `config.json`

### No Matching Videos

```
❌ No matching videos found.
```

**Possible causes:**
1. No videos matching your keywords in time range
2. Channels have no recent uploads
3. Keywords are too specific
4. `maxAgeDays` is too short

**Solutions:**
- Increase `maxAgeDays` (try 7 or 14)
- Add more keywords
- Add more channels
- Check if channels are posting content

### Rate Limit Errors

```
Error: quotaExceeded
```

**Solution:** YouTube API quota is 10,000 units/day. If you hit this limit:
1. Wait until tomorrow for quota to reset
2. Reduce number of channels
3. Increase `maxAgeDays` to reduce refresh frequency

## 📦 Skill Structure

```
youtube-ai-videos/
├── SKILL.md                 # Core skill documentation
├── config.json              # Configuration (channels, keywords, settings)
├── README.md               # This file
└── scripts/
    ├── fetch_youtube_ai_videos.py  # Main fetcher script
    └── find_channel_id.py        # Channel ID finder utility
```

## 📞 Support

- **OpenClaw Docs:** https://docs.openclaw.ai
- **ClawHub:** https://clawhub.com
- **Community:** https://discord.com/invite/clawd

## 📜 License

MIT License - feel free to use, modify, and distribute!

## 🤝 Contributing

Contributions are welcome! Feel free to:
- Add more AI-focused channels
- Improve filtering logic
- Add more output formats
- Fix bugs

## 🙏 Credits

- Built for OpenClaw users
- Uses YouTube Data API v3
- Inspired by the need for automated AI news aggregation

---

Made with ❤️ by the OpenClaw community


path: config.json
content: {
  "channels": [
    "@IchBinFabian",
    "@EverlastAI",
    "@BetterStack",
    "@ChristophMagnussen",
    "@Langdock",
    "@CodingWithLewis",
    "@AlexSprogis",
    "@FraunhoferIEM",
    "@RayFernando",
    "@TechWithTim",
    "@BrianCasel",
    "@IBMTechnology",
    "@SamWitteveen"
  ],
  "keywords": [
    "OpenClaw",
    "LLM",
    "Agent",
    "Claude Code",
    "OpenCode",
    "RAG"
  ],
  "maxVideos": 15,
  "maxAgeDays": 3,
  "searchTranscription": false,
  "youtubeApiKey": "YOUR_API_KEY_HERE"
}


path: scripts/fetch_youtube_ai_videos.py
content: #!/usr/bin/env python3
"""
YouTube AI Videos Fetcher (API-based with secure key storage)
Fetches recent AI-related videos using YouTube Data API v3
"""

import json
import sys
import os
from datetime import datetime, timedelta
from pathlib import Path

# Load config
config_path = Path(__file__).parent.parent / "config.json"
try:
    with open(config_path, 'r') as f:
        config = json.load(f)
except FileNotFoundError:
    print(f"Error: Config file not found at {config_path}")
    sys.exit(1)

CHANNELS = config.get('channels', [])
KEYWORDS = [k.lower() for k in config.get('keywords', [])]
MAX_VIDEOS = config.get('maxVideos', 15)
MAX_AGE_DAYS = config.get('maxAgeDays', 3)
SEARCH_TRANSCRIPTION = config.get('searchTranscription', False)

# SECURE API KEY LOADING (priority order)
# 1. Environment variable
# 2. Secrets file
# 3. Config (fallback)
YOUTUBE_API_KEY = None

# Try environment variable
YOUTUBE_API_KEY = os.environ.get('YOUTUBE_API_KEY')

# Try secrets file
if not YOUTUBE_API_KEY:
    secrets_dir = Path.home() / '.openclaw' / 'secrets'
    secrets_file = secrets_dir / 'youtube_api_key.txt'
    
    if secrets_file.exists():
        with open(secrets_file, 'r') as f:
            YOUTUBE_API_KEY = f.read().strip()

# Fallback to config (with warning)
if not YOUTUBE_API_KEY:
    YOUTUBE_API_KEY = config.get('youtubeApiKey', '')
    if YOUTUBE_API_KEY and YOUTUBE_API_KEY != 'YOUR_API_KEY_HERE':
        print("⚠️  Warning: Using API key from config.json. For security, use:")
        print("   export YOUTUBE_API_KEY='your_key'")
        print(f"   Or save to: {secrets_file}")

if not YOUTUBE_API_KEY or YOUTUBE_API_KEY == '' or YOUTUBE_API_KEY == 'YOUR_API_KEY_HERE':
    print("❌ YouTube Data API key is required!")
    print("Please add your API key:")
    print("  Option 1: export YOUTUBE_API_KEY='YOUR_KEY'")
    print(f"  Option 2: echo 'YOUR_KEY' > {secrets_file}")
    print("  Option 3: Get a free key at https://console.cloud.google.com/apis/api/youtube.googleapis.com")
    sys.exit(1)

def fetch_channel_videos(channel_input, api_key):
    """Fetch recent videos from a channel using YouTube Data API"""
    from urllib.request import urlopen, Request
    from urllib.parse import urlencode
    
    # Determine channel ID format
    if channel_input.startswith('@'):
        url = f"https://www.googleapis.com/youtube/v3/channels?part=id&forHandle={channel_input}&key={api_key}"
    else:
        url = f"https://www.googleapis.com/youtube/v3/channels?part=id&id={channel_input}&key={api_key}"
    
    try:
        req = Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        response = urlopen(req, timeout=10)
        data = json.loads(response.read().decode('utf-8'))
        
        if not data.get('items'):
            print(f"❌ Channel not found: {channel_input}")
            return []
        
        channel_id = data['items'][0]['id']
        
        # Fetch videos from channel
        videos_url = f"https://www.googleapis.com/youtube/v3/search?part=snippet&channelId={channel_id}&order=date&type=video&maxResults=25&key={api_key}"
        req = Request(videos_url, headers={'User-Agent': 'Mozilla/5.0'})
        response = urlopen(req, timeout=10)
        videos_data = json.loads(response.read().decode('utf-8'))
        
        videos = []
        for item in videos_data.get('items', []):
            video_id = item['id']['videoId']
            snippet = item['snippet']
            
            videos.append({
                'video_id': video_id,
                'title': snippet['title'],
                'published': snippet['publishedAt'],
                'channel': snippet['channelTitle'],
                'url': f"https://www.youtube.com/watch?v={video_id}"
            })
        
        return videos
    
    except Exception as e:
        print(f"Error fetching {channel_input}: {e}", file=sys.stderr)
        return []

def search_in_transcription(video_id, api_key):
    """Check if video contains keywords in transcription"""
    # This is complex and quota-heavy - skipped for now
    return None

def matches_keywords(title, transcription=None):
    """Check if title or transcription contains any keywords"""
    title_lower = title.lower()
    
    for keyword in KEYWORDS:
        if keyword in title_lower:
            return True, keyword
    
    return False, None

def time_ago(published_str):
    """Calculate time ago from ISO 8601 timestamp"""
    try:
        published = datetime.fromisoformat(published_str.replace('Z', '+00:00'))
        now = datetime.now(published.tzinfo)
        delta = now - published
        
        if delta.days > 0:
            return f"{delta.days}d ago"
        elif delta.seconds >= 3600:
            hours = delta.seconds // 3600
            return f"{hours}h ago"
        else:
            minutes = delta.seconds // 60
            return f"{minutes}m ago"
    except:
        return "unknown"

def is_within_age(published_str, max_days):
    """Check if video is within max age"""
    try:
        published = datetime.fromisoformat(published_str.replace('Z', '+00:00'))
        now = datetime.now(published.tzinfo)
        delta = now - published
        return delta.days <= max_days
    except:
        return False

def main():
    print(f"🎬 Fetching AI videos (last {MAX_AGE_DAYS} days)")
    print(f"Keywords: {', '.join(KEYWORDS)}")
    print(f"Channels: {len(CHANNELS)}")
    print()
    
    all_videos = []
    
    # Fetch videos from all channels
    for channel in CHANNELS:
        print(f"Fetching from {channel}...")
        videos = fetch_channel_videos(channel, YOUTUBE_API_KEY)
        all_videos.extend(videos)
    
    # Filter videos
    filtered_videos = []
    for video in all_videos:
        # Check age
        if not is_within_age(video['published'], MAX_AGE_DAYS):
            continue
        
        # Check keywords in title
        matches, matched_keyword = matches_keywords(video['title'])
        
        if not matches:
            continue
        
        video['matched_keyword'] = matched_keyword
        filtered_videos.append(video)
    
    # Sort by publication date (newest first)
    filtered_videos.sort(key=lambda x: x['published'], reverse=True)
    
    # Limit to max videos
    filtered_videos = filtered_videos[:MAX_VIDEOS]
    
    # Display results
    if not filtered_videos:
        print("❌ No matching videos found.")
        return
    
    for i, video in enumerate(filtered_videos, 1):
        time_ago_str = time_ago(video['published'])
        
        # Highlight matched keyword in title
        import re
        title = video['title']
        keyword = video.get('matched_keyword', '')
        if keyword:
            title = re.sub(f'({re.escape(keyword)})', r'**\1**', title, flags=re.IGNORECASE)
        
        print(f"{i}. [{time_ago_str}] [{title}]({video['url']})")
        print(f"   by @{video['channel']}")
        print()
    
    print(f"✅ Found {len(filtered_videos)} videos (from {len(CHANNELS)} channels)")

if __name__ == '__main__':
    main()


path: scripts/find_channel_id.py
content: #!/usr/bin/env python3
"""
YouTube Channel ID Finder
Finds YouTube channel IDs from channel handles (@name) or names
"""

import sys
import re
from urllib.parse import urlparse
from urllib.request import urlopen, Request

def find_channel_id(channel_input):
    """Find YouTube channel ID from handle or name"""
    
    # Convert handle to URL format
    if channel_input.startswith('@'):
        url = f"https://www.youtube.com/{channel_input}"
    elif channel_input.startswith('UC'):
        return channel_input  # Already a channel ID
    elif 'youtube.com/' in channel_input or 'youtu.be/' in channel_input:
        url = channel_input
    else:
        # Assume it's a channel name
        url = f"https://www.youtube.com/{channel_input}"
    
    try:
        # Fetch channel page
        req = Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        response = urlopen(req, timeout=10)
        html = response.read().decode('utf-8')
        
        # Extract channel ID from HTML
        # Channel IDs start with UC and are 24 characters
        pattern = r'"channelId":"(UC[A-Za-z0-9_-]{22})"'
        match = re.search(pattern, html)
        
        if match:
            return match.group(1)
        
        # Alternative patterns
        patterns = [
            r'youtube\.com/channel/(UC[A-Za-z0-9_-]{22})',
            r'"externalId":"(UC[A-Za-z0-9_-]{22})"',
        ]
        
        for pattern in patterns:
            match = re.search(pattern, html)
            if match:
                return match.group(1)
        
        print(f"❌ Could not find channel ID for: {channel_input}")
        return None
    
    except Exception as e:
        print(f"❌ Error fetching {url}: {e}")
        return None

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: find_channel_id.py '@channel_name' or 'channel_id' or 'url'")
        sys.exit(1)
    
    channel_input = sys.argv[1]
    channel_id = find_channel_id(channel_input)
    
    if channel_id:
        print(f"✅ Channel ID for '{channel_input}': {channel_id}")


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)