A comprehensive Python tool for synchronizing playlists between Spotify and YouTube Music with built-in API rate limiting and throttling.
- Spotify Integration: Secure authentication with automatic and manual fallback modes
- YouTube Music Integration: Full support for YouTube Music playlist operations
- API Rate Limiting: Advanced throttling mechanism to prevent rate limit violations
- Graceful Error Handling: Automatic retries with exponential backoff for failed requests
- User-Friendly: Simple setup with environment variable configuration
- Robust Authentication: Handles token refresh and authentication errors automatically
- Type Safety: Full type hints and mypy support
- Python 3.13 or higher
- Spotify Developer Account
- YouTube Music Account
# Clone the repository
git clone git@github.com:kodzonko/spo.git
cd spo
# Install dependencies
uv pip install -e .
# Install development dependencies (optional)
uv pip install -e ".[dev]"
- Create a web application on the Spotify Developer Dashboard
- Follow the Getting Started with Web API guide
- Your app dashboard should look like this:
Create an .env
file in the project root:
# Spotify Configuration
SPOTIFY_CLIENT_ID=your_spotify_client_id
SPOTIFY_CLIENT_SECRET=your_spotify_client_secret
SPOTIFY_REDIRECT_URI=http://localhost:8080/callback
from spo.spotify_client import SpotifyClient
# Initialize client with automatic throttling
with SpotifyClient() as client:
# Search for tracks (automatically throttled)
tracks = client.search_tracks("Bohemian Rhapsody", limit=10)
# Get user playlists (automatically throttled)
playlists = client.get_user_playlists(limit=20)
# Get saved tracks (automatically throttled)
saved_tracks = client.get_user_saved_tracks(limit=50)
from spo.youtube_music_client import YouTubeMusicClient
# Initialize YouTube Music client
with YouTubeMusicClient() as client:
# Search for tracks
tracks = client.search_tracks("Bohemian Rhapsody", limit=10)
# Get user playlists
playlists = client.get_user_playlists()
# Create or update playlists
playlist_id = client.create_playlist("My Synced Playlist", "Description")
This project includes a sophisticated throttling mechanism that prevents API rate limit violations and handles rate limits gracefully.
- Token Bucket Algorithm: Handles burst requests efficiently
- Sliding Window: Enforces per-minute rate limits
- Exponential Backoff: Automatic retry with increasing delays
- Spotify Optimization: Pre-configured limits optimized for Spotify's API
- Transparent Integration: No code changes required - just add decorators
All Spotify API methods are automatically decorated with @spotify_throttle()
, which:
- Prevents Rate Limits: Limits requests to 0.5/second and 30/minute by default
- Handles 429 Responses: Automatically retries with proper delays
- Manages Bursts: Allows up to 5 rapid requests before throttling
- Logs Activity: Provides detailed logging for monitoring and debugging
π Search 1/5: 'Bohemian Rhapsody'
β
Found 3 tracks in 0.45s
π Search 2/5: 'Billie Jean'
β
Found 3 tracks in 2.12s # Rate limited - waited before request
π Search 3/5: 'Hotel California'
β
Found 3 tracks in 2.08s
from spo.throttling import throttle, spotify_throttle
# Custom rate limits
@throttle(requests_per_second=1.0, max_retries=3)
def custom_api_call(self):
return self._spotify.some_endpoint()
# Spotify-optimized throttling
@spotify_throttle(requests_per_minute=20)
def conservative_api_call(self):
return self._spotify.another_endpoint()
try:
tracks = client.search_tracks("query")
except spotipy.SpotifyException as e:
if e.http_status == 429:
print("Rate limited (should be handled automatically)")
elif e.http_status == 401:
print("Authentication error")
else:
print(f"API error: {e}")
from spo.spotify_client import SpotifyClient
from spo.youtube_music_client import YouTubeMusicClient
# Sync a playlist from Spotify to YouTube Music
with SpotifyClient() as spotify, YouTubeMusicClient() as ytmusic:
# Get Spotify playlist
spotify_playlist = spotify.get_playlist_tracks("spotify_playlist_id")
# Create YouTube Music playlist
yt_playlist_id = ytmusic.create_playlist("Synced Playlist", "From Spotify")
# Add tracks to YouTube Music playlist
for track in spotify_playlist:
ytmusic.add_tracks_to_playlist(yt_playlist_id, [track])
Run the test suite:
# Run all tests
python -m pytest tests/
# Run specific test files
python -m pytest tests/test_spotify_client.py -v
python -m pytest tests/test_youtube_music_client.py -v
python -m pytest tests/test_throttling.py -v
# Run with coverage
python -m pytest tests/ --cov=spo --cov-report=html
spo/
βββ src/spo/
β βββ __init__.py
β βββ main.py # Main application entry point
β βββ spotify_client.py # Spotify API client with throttling
β βββ youtube_music_client.py # YouTube Music API client
β βββ throttling.py # Rate limiting and retry logic
β βββ auth_server.py # Authentication server
β βββ py.typed # Type hints marker
βββ tests/
β βββ conftest.py # Test configuration
β βββ test_spotify_client.py # Spotify client tests
β βββ test_youtube_music_client.py # YouTube Music client tests
β βββ test_throttling.py # Throttling mechanism tests
β βββ test_auth_server.py # Authentication tests
βββ docs/
β βββ spotify-app-dashboard.jpg # Spotify setup screenshot
βββ pyproject.toml # Project configuration
βββ uv.lock # Dependency lock file
βββ README.md
We welcome contributions! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Add tests for new functionality
- Ensure all tests pass (
python -m pytest tests/
) - Run linting (
ruff check src/ tests/
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
# Install development dependencies
uv pip install -e ".[dev]"
# Run pre-commit hooks
pre-commit install
# Run type checking
mypy src/
# Run linting
ruff check src/ tests/
ruff format src/ tests/
This project is licensed under the MIT License - see the LICENSE file for details.
- Spotipy - Spotify Web API wrapper
- ytmusicapi - YouTube Music API wrapper
- Loguru - Advanced logging library