Automatically sync your latest YouTube subscription uploads into a custom playlist using the YouTube Data API v3.
A modular Python package that creates or updates a playlist with your most recent subscribed content β filtered, customizable, and cron-friendly.
- Web Dashboard Interface - Modern UI for managing playlists, configuration, and channel filtering
- Quota-optimized API usage - Batched operations reduce quota consumption by 95%+
- Automated quota tracking - Real-time API call monitoring with detailed usage analysis
- Intelligent duplicate detection - Pre-insertion caching prevents wasted API calls
- Flexible video filtering:
- Duration range filtering (minimum and maximum duration support)
- Live content filtering (livestreams/premieres)
- Channel allowlist/blocklist support (three modes: none, allowlist, blocklist)
- Custom filtering rules
- Channel Management UI - Visual interface for managing channel filters with real-time search
- Robust error handling - Retry logic and graceful degradation
- Multiple interfaces - Web dashboard, command-line tool, and shell scripts
- Comprehensive reporting - CSV reports with detailed metadata
- Automated scheduling - Cron-friendly design with proper logging
- Dynamic quota management - Centralized cost configuration with intelligent fallbacks
- Configuration Management - JSON-based config with web UI and precedence system
- Python 3.8+
- YouTube Data API v3 access
- Google Cloud project with:
- OAuth 2.0 client ID + secret
- YouTube Data API enabled
```bash
git clone https://github.com/yourusername/yt-sub-playlist.git
cd yt-sub-playlist
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
-
Google Cloud OAuth credentials
- Visit https://console.cloud.google.com/
- Create a new project β enable YouTube Data API v3
- Create OAuth 2.0 credentials (desktop type)
- Download
client_secrets.jsonand place it in the project root
-
Create .env file
cp .env.example .env
-
Run Initial Auth Flow
python -m yt_sub_playlist.auth.oauth
This will open a browser window to authorize your Google account and store
token.json.
The codebase is organized as a modular Python package:
```
yt_sub_playlist/
βββ __main__.py # CLI entry point
βββ __init__.py
βββ core/
β βββ __init__.py
β βββ youtube_client.py # YouTube API wrapper with quota optimization
β βββ video_filtering.py # Video filtering and processing logic
β βββ playlist_manager.py # High-level playlist orchestration
β βββ quota_tracker.py # Quota management and estimation
βββ auth/
β βββ __init__.py
β βββ oauth.py # OAuth2 authentication handling
βββ config/
β βββ __init__.py
β βββ env_loader.py # Environment configuration management
β βββ schema.py # Configuration validation and defaults
β βββ quota_costs.py # YouTube API quota cost management
β βββ youtube_quota_costs.json # Centralized quota cost configuration
βββ data/
β βββ processed_videos.json # Cache of processed video IDs
β βββ playlist_cache/ # Cached playlist contents
β βββ api_call_log.json # (Ignored) Real-time API usage tracking; not committed
β βββ logs/ # Application logs
βββ scripts/
β βββ run.sh # Production runner script
β βββ dryrun.sh # Dry-run testing script
β βββ reset-auth.sh # Authentication reset utility
β βββ quota_test.sh # Quota usage simulator runner
β βββ quota_simulator.py # Quota estimation utility
βββ reports/
βββ videos_added.csv # Generated reports
```
core/youtube_client.py- Low-level YouTube API operations with batching, caching, and automated call trackingcore/video_filtering.py- Video filtering logic and criteria managementcore/playlist_manager.py- High-level workflow orchestrationcore/quota_tracker.py- Quota management and estimation utilitiesauth/oauth.py- OAuth2 flow and credential managementconfig/env_loader.py- Configuration loading and validationconfig/schema.py- Configuration contracts and defaultsconfig/quota_costs.py- YouTube API quota cost management and loading
The easiest way to use yt-sub-playlist is through the web dashboard:
# Start the dashboard server
cd dashboard/backend && python app.py
# Open in your browser
# http://localhost:5001Dashboard Features:
- Main Dashboard (
/) - View playlist statistics, quota usage, and manage playlists - Settings (
/config.html) - Configure all options with visual controls and validation - Channel Manager (
/channels.html) - Manage channel allowlist/blocklist with search and filtering
# Normal run - adds new videos to playlist
python -m yt_sub_playlist
# Dry-run mode (shows what would be added without making changes)
python -m yt_sub_playlist --dry-run
# Verbose logging for debugging
python -m yt_sub_playlist --verbose
# Limit number of videos to process
python -m yt_sub_playlist --limit 20
# Generate CSV report
python -m yt_sub_playlist --report reports/videos_added.csvConvenient shell scripts are provided for common operations:
# Production run with logging
./yt_sub_playlist/scripts/run.sh
# Dry-run mode with verbose output
./yt_sub_playlist/scripts/dryrun.sh
# Reset authentication tokens
./yt_sub_playlist/scripts/reset-auth.sh
# Run quota usage simulator
./yt_sub_playlist/scripts/quota_test.shAll scripts are automatically made executable during setup.
Generate detailed CSV reports of all processed videos:
# Generate report with video metadata
python -m yt_sub_playlist --report reports/videos_added.csv
# Combine with dry-run to preview what would be processed
python -m yt_sub_playlist --dry-run --report reports/preview.csvCSV Fields: title, video_id, channel_title, channel_id, published_at, duration_seconds, live_broadcast, added
Two Configuration Methods:
-
Web Dashboard (Recommended): Start the dashboard and use the Settings page to manage configuration visually:
cd dashboard/backend && python app.py # Open http://localhost:5001/config.html
-
Manual Configuration: Edit configuration files directly (see below)
Configuration Sources & Precedence:
The system loads configuration from multiple sources with the following priority (highest to lowest):
- CLI arguments (e.g.,
--limit 10) - Environment variables (
.envfile) - for secrets and overrides - User preferences (
config.jsonfile) - managed via dashboard - Built-in defaults
Environment Variables (.env):
Use for secrets and system-specific overrides:
# API credentials (secrets - must be in .env)
PLAYLIST_ID=PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Optional: use existing playlist
# User preferences (can be overridden in config.json via dashboard)
PLAYLIST_NAME="My Auto Playlist" # Name for new playlists
PLAYLIST_VISIBILITY=unlisted # private, unlisted, or public
VIDEO_MIN_DURATION_SECONDS=120 # Skip videos shorter than 2 minutes
SKIP_LIVE_CONTENT=true # Skip livestreams and premieres
CHANNEL_ID_WHITELIST=UC1234,UC5678 # Optional: only include these channels
LOOKBACK_HOURS=24 # How far back to look for videos
MAX_VIDEOS_TO_FETCH=50 # Maximum videos to process per runUser Preferences (config.json):
Automatically created when using the dashboard Settings page. Example:
{
"playlist_name": "Auto Playlist from Subscriptions",
"playlist_visibility": "unlisted",
"min_duration_seconds": 120,
"max_duration_seconds": null,
"lookback_hours": 48,
"max_videos": 50,
"skip_live_content": true,
"channel_filter_mode": "none",
"channel_allowlist": null,
"channel_blocklist": null
}Note: Values in .env take precedence over config.json. This allows you to override dashboard settings when needed.
Control which channels contribute videos to your playlist using three mutually exclusive modes:
1. None (Default)
- All subscribed channels are included
- No filtering applied
2. Allowlist Mode
- Only channels in the allowlist contribute videos
- All other channels are excluded
- Useful when you want content from specific creators only
3. Blocklist Mode
- All channels except those in the blocklist contribute videos
- Useful for excluding specific channels while including everyone else
Via Web Dashboard (Recommended):
- Start the dashboard:
cd dashboard/backend && python app.py - Navigate to Settings β Channel Filtering β "Manage Channels"
- Select your filter mode (none/allowlist/blocklist)
- Search and select channels
- Save configuration
Via config.json:
{
"channel_filter_mode": "allowlist",
"channel_allowlist": ["UCxxxxxx1", "UCxxxxxx2"],
"channel_blocklist": null
}Via Environment Variables:
# Allowlist mode
CHANNEL_FILTER_MODE=allowlist
CHANNEL_ALLOWLIST=UCxxxxxx1,UCxxxxxx2
# Blocklist mode
CHANNEL_FILTER_MODE=blocklist
CHANNEL_BLOCKLIST=UCxxxxxx3,UCxxxxxx4The legacy channel_whitelist configuration is automatically migrated to channel_allowlist when using the new filter system. Your existing whitelist will continue to work seamlessly.
Allowlist Examples:
- Create a playlist from your top 5 favorite channels
- Focus on educational content creators only
- Build a curated playlist for a specific topic
Blocklist Examples:
- Exclude gaming channels from your general playlist
- Remove channels that post too frequently
- Filter out channels with content you've already watched elsewhere
Control the length of videos included in your playlist using minimum and maximum duration filters.
Set both minimum and maximum duration limits to create precisely filtered playlists:
- Minimum Duration (
min_duration_seconds): Skip videos shorter than this (default: 60 seconds) - Maximum Duration (
max_duration_seconds): Skip videos longer than this (default: unlimited/null)
Via Web Dashboard:
- Navigate to Settings page (
http://localhost:5001/config.html) - Adjust the "Min Duration" slider (range: 1s - 7200s)
- Adjust the "Max Duration" slider (range: 60s - 7200s)
- Check "Unlimited" for no maximum duration limit
- Click "Save Configuration"
Via config.json:
{
"min_duration_seconds": 120,
"max_duration_seconds": 1800
}Set max_duration_seconds to null for unlimited maximum duration.
Skip Shorts AND Long Videos:
- Min: 300s (5 minutes), Max: 1200s (20 minutes)
- Perfect for focused, mid-length content
Quick Consumption Playlist:
- Min: 60s (1 minute), Max: 600s (10 minutes)
- Great for quick viewing sessions
Deep Dive Content Only:
- Min: 1800s (30 minutes), Max: null (unlimited)
- Focus on in-depth tutorials and lectures
No Lengthy Content:
- Min: 60s (1 minute), Max: 900s (15 minutes)
- Avoid time-consuming videos
The filtering process tracks duration-based exclusions:
- too_short: Videos rejected for being under minimum duration
- too_long: Videos rejected for exceeding maximum duration
These statistics appear in logs and help tune your duration range settings.
Control which videos are included based on their publish date using flexible date filtering modes.
Choose from three different date filtering approaches:
1. Lookback Mode (default)
- Uses
lookback_hoursparameter for hourly precision - Example: "Last 24 hours", "Last 48 hours"
- Best for: Frequent updates, recent content focus
2. Days Mode
- Filter by "last N days" with daily granularity
- Range: 1-365 days
- Cutoff is start of day (00:00:00)
- Example: "Last 7 days" includes all videos from 7 days ago at midnight to now
- Best for: Weekly/monthly playlists, consistent rolling windows
3. Date Range Mode
- Filter by specific start and end dates
- Date picker interface (YYYY-MM-DD format)
- End date includes full day (23:59:59)
- Example: "October 1-27, 2025"
- Best for: Event coverage, monthly archives, specific time periods
Via Web Dashboard:
- Navigate to Settings page (
http://localhost:5001/config.html) - Select "Date Filter Mode" from dropdown
- Configure based on selected mode:
- Lookback: Adjust hours slider (1-168 hours)
- Days: Enter number of days (1-365)
- Date Range: Select start and end dates
- Click "Save Configuration"
Via config.json:
{
"date_filter_mode": "days",
"date_filter_days": 7,
"date_filter_start": null,
"date_filter_end": null
}Or for date range mode:
{
"date_filter_mode": "date_range",
"date_filter_days": null,
"date_filter_start": "2025-10-01",
"date_filter_end": "2025-10-27"
}Weekly Content Roundup:
- Mode: days
- Days: 7
- Result: Rolling 7-day window for weekly playlists
Monthly Archives:
- Mode: date_range
- Start: 2025-10-01, End: 2025-10-31
- Result: All October 2025 videos
Recent Updates Only:
- Mode: lookback
- Hours: 24
- Result: Videos from last 24 hours (backward compatible)
Event Coverage:
- Mode: date_range
- Start: 2025-10-15, End: 2025-10-20
- Result: Videos published during specific event dates
The filtering process tracks date-based exclusions:
- outside_date_range: Videos rejected for being outside the configured date filter
These statistics appear in logs and show which date filter mode is active.
Filter videos based on keywords found in their title or description, with flexible matching options.
Choose from four filtering approaches:
1. None (default)
- No keyword filtering applied
- All videos pass keyword filter
2. Include Mode (whitelist)
- Videos must contain specific keyword(s)
- Match type options:
- Any: Video needs at least one keyword (OR logic)
- All: Video needs every keyword (AND logic)
- Example: Include ["tutorial", "guide"] with "any" = videos containing "tutorial" OR "guide"
3. Exclude Mode (blacklist)
- Videos must NOT contain any exclude keywords
- Always uses OR logic (any match = reject)
- Example: Exclude ["spoiler", "ending"] = reject videos with "spoiler" OR "ending"
4. Both Mode (combined)
- Must pass include check (with any/all logic)
- Must NOT match any exclude keywords
- Example: Include ["review"] + Exclude ["spoiler"] = reviews without spoilers
Via Web Dashboard:
- Navigate to Settings page (
http://localhost:5001/config.html) - Select "Keyword Filter Mode" from dropdown
- Enter keywords (one per line) in appropriate text area:
- Include Keywords: For include/both modes
- Exclude Keywords: For exclude/both modes
- Configure advanced options (for include/both modes):
- Match Type: "any" (OR) or "all" (AND) for include keywords
- Case Sensitive: Enable for exact case matching
- Search Description: Enable to search video descriptions (not just titles)
- Click "Save Configuration"
Via config.json:
{
"keyword_filter_mode": "include",
"keyword_include": ["tutorial", "guide", "how to"],
"keyword_exclude": null,
"keyword_match_type": "any",
"keyword_case_sensitive": false,
"keyword_search_description": false
}Or for both mode:
{
"keyword_filter_mode": "both",
"keyword_include": ["review", "analysis"],
"keyword_exclude": ["spoiler", "clickbait"],
"keyword_match_type": "any",
"keyword_case_sensitive": false,
"keyword_search_description": false
}Tutorial Content Only:
- Mode: include
- Keywords: ["tutorial", "guide", "how to"]
- Match: any
- Result: Only educational/instructional content
Avoid Spoilers:
- Mode: exclude
- Keywords: ["spoiler", "ending", "finale", "twist"]
- Result: No spoiler-containing videos
Specific Topic (Strict):
- Mode: include
- Keywords: ["python", "tutorial"]
- Match: all
- Result: Only videos with BOTH "python" AND "tutorial"
Curated Reviews:
- Mode: both
- Include: ["review", "analysis"]
- Exclude: ["spoiler", "unboxing"]
- Result: Reviews and analysis without spoilers or unboxings
Case-Sensitive Filtering:
- Mode: include
- Keywords: ["Python"] (capital P)
- Case Sensitive: enabled
- Result: Matches "Python" but not "python"
Deep Search:
- Mode: include
- Keywords: ["machine learning"]
- Search Description: enabled
- Result: Finds keyword in title OR description
Match Type (for include mode):
- Any (OR): Video needs at least one include keyword
- All (AND): Video needs every include keyword
Case Sensitive:
- Default: false (case-insensitive matching)
- When enabled: "Python" β "python"
Search Description:
- Default: false (title only)
- When enabled: searches both title and description text
The filtering process tracks keyword-based exclusions:
- keyword_filtered_include: Videos rejected for not matching include keywords
- keyword_filtered_exclude: Videos rejected for matching exclude keywords
These statistics appear in logs and help tune your keyword filters.
All filters use AND logic - videos must pass every enabled filter to be included in the playlist.
Filtering Pipeline:
- Already Processed Check: Skip videos already in playlist (cache lookup)
- Duration Filter: Check min/max duration constraints
- Date Filter: Check publish date against selected mode (lookback/days/date_range)
- Channel Filter: Check allowlist/blocklist (if enabled)
- Keyword Filter: Check include/exclude keywords (if enabled)
- Live Content Filter: Skip livestreams/premieres (if enabled)
Filter Combination Example:
{
"min_duration_seconds": 300,
"max_duration_seconds": 1200,
"date_filter_mode": "days",
"date_filter_days": 7,
"channel_filter_mode": "allowlist",
"channel_allowlist": ["UCxxxxx", "UCyyyyy"],
"keyword_filter_mode": "both",
"keyword_include": ["tutorial"],
"keyword_exclude": ["beginner"],
"skip_live_content": true
}This configuration creates a playlist with:
- β Videos between 5-20 minutes long
- β Published in the last 7 days
- β From specific whitelisted channels
- β Containing "tutorial" in title
- β NOT containing "beginner" in title
- β No livestreams or premieres
Result: Intermediate/advanced tutorials from trusted creators, published recently.
Statistics Output:
The filtering process logs statistics for each stage:
Video filtering stats:
Total videos: 50
Already processed: 5
Too short (<300s): 8
Too long (>1200s): 6
Outside date range (days mode): 4
Not in allowlist: 12
Keyword filtered (include): 3
Keyword filtered (exclude): 2
Live content skipped: 1
Passed filters: 9
Run the script on a schedule using cron:
# Edit your crontab
crontab -e
# Add entry to run every 2 hours
0 */2 * * * cd /path/to/yt-sub-playlist && ./yt_sub_playlist/scripts/run.shFor more complex scheduling needs, consider using:
- systemd timers (Linux)
- Task Scheduler (Windows)
- APScheduler (Python-based)
- GitHub Actions (cloud-based)
This tool is designed to minimize YouTube API quota usage with intelligent monitoring:
- Batched operations - Up to 50 videos per API call
- Smart caching - 12-hour TTL for playlist contents
- Duplicate detection - Pre-insertion filtering prevents waste
- Efficient subscription handling - Uses uploads playlist lookup vs expensive search
- Real-time API monitoring - Every API call is automatically tracked
- Dynamic quota analysis - Live usage data replaces static estimates
- Centralized cost management - All quota costs stored in
config/youtube_quota_costs.json - Intelligent reporting - Detailed per-method breakdown with call counts and costs
Run the quota simulator to analyze your actual API usage:
# After running the main application, analyze real usage
./yt_sub_playlist/scripts/quota_test.sh
# Or run directly
python yt_sub_playlist/scripts/quota_simulator.pySample output:
π Loaded API call counts from yt_sub_playlist/data/api_call_log.json
π Quota Usage Analysis:
channels.list : 15 calls Γ 1 units = 15 units
subscriptions.list : 3 calls Γ 1 units = 3 units
playlistItems.list : 12 calls Γ 1 units = 12 units
videos.list : 8 calls Γ 1 units = 8 units
playlistItems.insert : 45 calls Γ 50 units = 2250 units
playlists.list : 1 calls Γ 1 units = 1 units
π’ Total Estimated Usage: 2289 / 10000 units
Typical quota usage:
- Before optimization: ~8,000 units per run
- After optimization: ~500-1,000 units per run
- ~85-90% quota reduction
All YouTube API quota costs are managed in config/youtube_quota_costs.json:
{
"channels.list": 1,
"playlistItems.insert": 50,
"playlistItems.list": 1,
"playlists.list": 1,
"subscriptions.list": 1,
"videos.list": 1,
"search.list": 100,
"playlists.insert": 50
}The system automatically loads quota costs at runtime with intelligent fallbacks:
from yt_sub_playlist.config.quota_costs import get_quota_cost
# Automatically loads from JSON config with fallback to default (1)
cost = get_quota_cost("videos.list") # Returns: 1
cost = get_quota_cost("playlistItems.insert") # Returns: 50
cost = get_quota_cost("unknown.method") # Returns: 1 (with warning log)Every API call is automatically tracked without manual intervention:
- Real-time counting - All YouTube API methods are instrumented
- Persistent logging - Usage data saved to
data/api_call_log.json - Session tracking - Counters persist across application runs
- Error resilience - Tracking continues even if individual API calls fail
- Run your application:
python -m yt_sub_playlist - API calls are tracked automatically - No configuration needed
- Analyze real usage:
./yt_sub_playlist/scripts/quota_test.sh - Optimize based on data - Focus on high-cost operations
The following files are automatically generated at runtime and excluded from version control:
data/playlist_cache/*.jsonβ Cached playlist contents (used to prevent reprocessing and reduce API quota usage)data/api_call_log.jsonβ Real-time API usage tracking used by the quota simulator
These are listed in
.gitignoreto ensure they are not committed.
# Reset stored tokens and re-authenticate
./yt_sub_playlist/scripts/reset-auth.sh
# Test authentication manually
python -m yt_sub_playlist.auth.oauth# Validate your .env configuration
python -c "
from yt_sub_playlist.config.env_loader import load_config
from yt_sub_playlist.config.schema import ConfigSchema
config = load_config()
print(ConfigSchema.get_config_summary(config))
"If you hit quota limits:
- Wait until midnight Pacific Time for quota reset
- Reduce
LOOKBACK_HOURSto process fewer videos - Use
--limitflag to restrict processing - Check quota usage in Google Cloud Console
# Test core functionality
python -m pytest tests/
# Test specific modules
python -m pytest tests/test_video_filtering.py -vThe modular design supports:
- Easy testing - Each module has clear responsibilities
- Flexible configuration - Environment-based settings
- Extensible filtering - Add custom video criteria
- Multiple interfaces - CLI, scripts, or direct imports
- Web dashboard interface (Phases 2-4)
- Channel allowlist/blocklist filtering (Phase 4)
- Configuration management UI (Phase 3)
- Real-time quota tracking and monitoring
- CSV reporting with detailed metadata
- Automated duplicate detection
- Scheduling system with cron integration
- Analytics dashboard (playlist growth, channel stats)
- Multi-playlist support
- Advanced video filtering rules (duration ranges, keyword filters, date ranges)
- Notification integrations (email, Slack, Discord)
- Playlist cleanup (remove old videos)
- Docker container support
- Channel metadata display (thumbnails, subscriber counts)
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes using Conventional Commits format:
git commit -m 'feat(scope): add amazing feature' - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project uses Conventional Commits for automatic changelog generation:
feat:- New featuresfix:- Bug fixesrefactor:- Code refactoringdocs:- Documentation changestest:- Test additionschore:- Maintenance tasks
Examples:
git commit -m "feat(ui): add dark mode support"
git commit -m "fix(api): resolve quota tracking issue"
git commit -m "docs(readme): update installation instructions"See docs/RELEASE_PROCESS.md for details on changelog automation.
This project is licensed under the MIT License - see the LICENSE file for details.