├── .gitignore ├── bakup.pkl ├── requirements.txt ├── README.md ├── README ├── yt2spotify_README.md ├── API_SETUP.md └── spotify2yt_README.md ├── yt2spotify.py └── spotify2yt.py /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .env 3 | client_secrets.json 4 | token.pickle 5 | bakup.pkl -------------------------------------------------------------------------------- /bakup.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/punkholic/YouTube-to-Spotify-Paylist/HEAD/bakup.pkl -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | spotipy==2.23.0 2 | python-dotenv==1.0.0 3 | ytmusicapi==0.24.1 4 | google-api-python-client==2.108.0 5 | google-auth-oauthlib==1.1.0 6 | google-auth-httplib2==0.1.1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotify-YouTube Playlist Migrator 2 | 3 | A collection of scripts to migrate playlists between Spotify and YouTube. This repository contains two main scripts: 4 | 5 | 6 | ## Navigation 7 | 8 | - [API Setup Guide](README/API_SETUP.md) - Detailed instructions for setting up Spotify and YouTube API credentials 9 | - [YouTube to Spotify Guide](README/yt2spotify_README.md) - Complete guide for migrating from YouTube to Spotify 10 | - [Spotify to YouTube Guide](README/spotify2yt_README.md) - Complete guide for migrating from Spotify to YouTube 11 | 12 | ## Overview 13 | 14 | This project provides tools to help you transfer your music playlists between Spotify and YouTube. Whether you're moving from one platform to another or just want to have your playlists available on both platforms, these scripts make the process easy and automated. 15 | 16 | ## Features 17 | 18 | - Bidirectional playlist migration 19 | - Maintains song order 20 | - Handles rate limiting 21 | - Batch processing 22 | - Progress tracking 23 | - Error handling 24 | - Private playlist support 25 | 26 | ## Quick Start 27 | 28 | 1. Clone this repository: 29 | ```bash 30 | git clone https://github.com/punkholic/spotify-youtube-migrator.git 31 | cd spotify-youtube-migrator 32 | ``` 33 | 34 | 2. Install dependencies: 35 | ```bash 36 | pip install -r requirements.txt 37 | ``` 38 | 39 | 3. Set up API credentials: 40 | - See [API Setup Guide](README/API_SETUP.md) for detailed instructions on setting up Spotify and YouTube API credentials 41 | - Note: The setup uses an ngrok URL for OAuth redirects to ensure reliable authentication flow 42 | 43 | 4. Choose your migration direction: 44 | - For YouTube to Spotify: See [YouTube to Spotify Guide](README/yt2spotify_README.md) 45 | - For Spotify to YouTube: See [Spotify to YouTube Guide](README/spotify2yt_README.md) 46 | 47 | ## Requirements 48 | 49 | - Python 3.6 or higher 50 | - Spotify account 51 | - Google account 52 | - YouTube Data API v3 enabled 53 | - Required Python packages (see requirements.txt) 54 | 55 | ## Project Structure 56 | 57 | ``` 58 | spotify-youtube-migrator/ 59 | ├── README.md # This file 60 | ├── README/ # Documentation directory 61 | │ ├── API_SETUP.md # API credentials setup guide 62 | │ ├── yt2spotify_README.md # YouTube to Spotify documentation 63 | │ └── spotify2yt_README.md # Spotify to YouTube documentation 64 | ├── yt2spotify.py # YouTube to Spotify script 65 | ├── spotify2yt.py # Spotify to YouTube script 66 | ├── requirements.txt # Python dependencies 67 | └── .env # Environment variables (create this) 68 | ``` 69 | 70 | ## Contributing 71 | 72 | Contributions are welcome! Please feel free to submit a Pull Request. 73 | 74 | ## License 75 | 76 | This project is licensed under the MIT License - see the LICENSE file for details. 77 | 78 | ## Acknowledgments 79 | 80 | - [Spotipy](https://github.com/spotipy-dev/spotipy) - Spotify Web API wrapper 81 | - [Google API Python Client](https://github.com/googleapis/google-api-python-client) - YouTube Data API wrapper -------------------------------------------------------------------------------- /README/yt2spotify_README.md: -------------------------------------------------------------------------------- 1 | # YouTube to Spotify Playlist Migrator 2 | 3 | This script allows you to migrate your YouTube playlists to Spotify playlists. It uses the YouTube Data API (via yt-dlp) and Spotify Web API to transfer your music. 4 | 5 | ## Prerequisites 6 | 7 | - Python 3.6 or higher 8 | - A Spotify account 9 | - A YouTube playlist URL 10 | - YouTube Data API v3 enabled in Google Cloud Console 11 | 12 | ## Installation 13 | 14 | 1. Install the required dependencies: 15 | ```bash 16 | pip install -r requirements.txt 17 | ``` 18 | 19 | 2. Set up Spotify credentials: 20 | - Create a `.env` file in the project directory 21 | - Add your Spotify credentials: 22 | ``` 23 | SPOTIPY_CLIENT_ID=your_spotify_client_id 24 | SPOTIPY_CLIENT_SECRET=your_spotify_client_secret 25 | SPOTIPY_REDIRECT_URI=your_spotify_redirect_uri 26 | ``` 27 | 28 | ## Usage 29 | 30 | 1. Basic usage (with default playlist name): 31 | ```bash 32 | python yt2spotify.py --playlist_url "https://www.youtube.com/playlist?list=YOUR_PLAYLIST_ID" 33 | ``` 34 | 35 | 2. With custom playlist name: 36 | ```bash 37 | python yt2spotify.py --playlist_url "https://www.youtube.com/playlist?list=YOUR_PLAYLIST_ID" --playlist_name "My Custom Playlist" 38 | ``` 39 | 40 | 3. Delete existing playlist with same name: 41 | ```bash 42 | python yt2spotify.py --playlist_url "https://www.youtube.com/playlist?list=YOUR_PLAYLIST_ID" --playlist_name "My Custom Playlist" --delete 43 | ``` 44 | 45 | Command-line Arguments: 46 | - `--playlist_url` (required): The URL of your YouTube playlist 47 | - `--playlist_name`: Name for the Spotify playlist (default: "youtube") 48 | - `--delete`: Delete existing playlist with same name without prompting 49 | 50 | The script will: 51 | - Extract song titles from the YouTube playlist 52 | - Clean and parse the titles to extract artist and song names 53 | - Search for each song on Spotify 54 | - Create a new private Spotify playlist (or use existing one) 55 | - Add the found songs to the playlist 56 | - Save progress to a pickle file for resuming if needed 57 | 58 | ## Features 59 | 60 | - Migrates entire YouTube playlists to Spotify 61 | - Smart title parsing to extract artist and song names 62 | - Handles rate limiting and retries 63 | - Creates private playlists by default 64 | - Batch processing to avoid API limits 65 | - Progress saving and resuming 66 | - Duplicate track prevention 67 | - Error handling and retries 68 | 69 | ## Notes 70 | 71 | - The script creates a pickle file to store track URIs for resuming 72 | - Spotify playlists are created as private by default 73 | - The script uses the first search result for each song 74 | - Rate limiting is implemented to avoid API quota issues 75 | - Title parsing removes common YouTube video indicators (e.g., "official", "lyrics", "video") 76 | 77 | ## Troubleshooting 78 | 79 | 1. If songs aren't being found: 80 | - The title parsing might not be accurate for some formats 81 | - Try modifying the `extract_song_name` function for your specific playlist format 82 | 83 | 2. If you get authentication errors: 84 | - Make sure your Spotify credentials are correct in the `.env` file 85 | - Verify that you've granted the necessary permissions 86 | 87 | 3. If the script stops unexpectedly: 88 | - Check the pickle file for saved progress 89 | - Run the script again with the same parameters to resume 90 | 91 | ## License 92 | 93 | This project is licensed under the MIT License - see the LICENSE file for details. -------------------------------------------------------------------------------- /README/API_SETUP.md: -------------------------------------------------------------------------------- 1 | # API Setup Guide 2 | 3 | This guide will walk you through setting up the necessary API credentials for both Spotify and YouTube Data API v3. 4 | 5 | ## Spotify API Setup 6 | 7 | 1. Go to [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/) 8 | 2. Log in with your Spotify account 9 | 3. Click "Create App" 10 | 4. Fill in the app details: 11 | - App name: "Spotify-YouTube Migrator" (or any name you prefer) 12 | - App description: "A tool to migrate playlists between Spotify and YouTube" 13 | - Website: (Optional) Your website or GitHub repo 14 | - Redirect URI: `https://7cfe-2400-1a00-1b2c-5edf-4042-f862-4f9a-3909.ngrok-free.app/callback` 15 | 5. Accept the terms and click "Create" 16 | 6. Once created, you'll see your: 17 | - Client ID 18 | - Client Secret 19 | 7. Add these to your `.env` file: 20 | ``` 21 | SPOTIPY_CLIENT_ID=your_client_id_here 22 | SPOTIPY_CLIENT_SECRET=your_client_secret_here 23 | SPOTIPY_REDIRECT_URI=https://7cfe-2400-1a00-1b2c-5edf-4042-f862-4f9a-3909.ngrok-free.app/callback 24 | ``` 25 | 26 | ## YouTube Data API v3 Setup 27 | 28 | 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 29 | 2. Create a new project or select an existing one 30 | 3. Enable the YouTube Data API v3: 31 | - Go to "APIs & Services" > "Library" 32 | - Search for "YouTube Data API v3" 33 | - Click "Enable" 34 | 35 | 4. Configure OAuth consent screen: 36 | - Go to "APIs & Services" > "OAuth consent screen" 37 | - Choose "External" user type 38 | - Fill in the required information: 39 | - App name: "Spotify-YouTube Migrator" 40 | - User support email: Your email 41 | - Developer contact information: Your email 42 | - Click "Save and Continue" 43 | - Add the scope: `https://www.googleapis.com/auth/youtube` 44 | - Click "Save and Continue" 45 | - Add test users: 46 | - Click "ADD USERS" 47 | - Enter your email address (the one you'll use to run the script) 48 | - Click "ADD" 49 | - You can add up to 100 test users 50 | - Make sure to add all email addresses that will use the application 51 | - Click "Save and Continue" 52 | - Review your settings and click "Back to Dashboard" 53 | 54 | > **Important**: If you don't add your email as a test user, you'll get an "access_denied" error when trying to use the application. Only test users can access the application while it's in testing mode. 55 | 56 | 5. Create OAuth 2.0 credentials: 57 | - Go to "APIs & Services" > "Credentials" 58 | - Click "Create Credentials" > "OAuth client ID" 59 | - Choose "Desktop app" as the application type 60 | - Name: "Spotify-YouTube Migrator" 61 | - Click "Create" 62 | - Download the credentials (JSON file) 63 | - Rename the downloaded file to `client_secrets.json` 64 | - Place it in your project directory 65 | 66 | 6. Configure authorized redirect URIs: 67 | - Go back to your OAuth 2.0 Client ID 68 | - Click the edit (pencil) icon 69 | - Under "Authorized redirect URIs", add: 70 | - `https://7cfe-2400-1a00-1b2c-5edf-4042-f862-4f9a-3909.ngrok-free.app/` 71 | - Click "Save" 72 | 73 | ## Verifying Your Setup 74 | 75 | 1. Check your `.env` file has the correct Spotify credentials: 76 | ``` 77 | SPOTIPY_CLIENT_ID=your_spotify_client_id 78 | SPOTIPY_CLIENT_SECRET=your_spotify_client_secret 79 | SPOTIPY_REDIRECT_URI=https://7cfe-2400-1a00-1b2c-5edf-4042-f862-4f9a-3909.ngrok-free.app/callback 80 | ``` 81 | 82 | 2. Verify `client_secrets.json` -------------------------------------------------------------------------------- /README/spotify2yt_README.md: -------------------------------------------------------------------------------- 1 | # Spotify to YouTube Playlist Migrator 2 | 3 | This script allows you to migrate your Spotify playlists or liked songs to YouTube playlists. It uses the Spotify Web API and YouTube Data API v3 to transfer your music. 4 | 5 | ## Prerequisites 6 | 7 | - Python 3.6 or higher 8 | - A Spotify account 9 | - A Google account 10 | - YouTube Data API v3 enabled in Google Cloud Console 11 | 12 | ## Installation 13 | 14 | 1. Install the required dependencies: 15 | ```bash 16 | pip install -r requirements.txt 17 | ``` 18 | 19 | 2. Set up Spotify credentials: 20 | - Create a `.env` file in the project directory 21 | - Add your Spotify credentials: 22 | ``` 23 | SPOTIPY_CLIENT_ID=your_spotify_client_id 24 | SPOTIPY_CLIENT_SECRET=your_spotify_client_secret 25 | SPOTIPY_REDIRECT_URI=your_spotify_redirect_uri 26 | ``` 27 | 28 | 3. Set up YouTube API credentials: 29 | - Go to [Google Cloud Console](https://console.cloud.google.com/) 30 | - Create a new project or select an existing one 31 | - Enable the YouTube Data API v3 32 | - Go to "Credentials" 33 | - Create OAuth 2.0 Client ID (Desktop application) 34 | - Download the credentials and save as `client_secrets.json` in the project directory 35 | 36 | 4. Configure OAuth consent screen: 37 | - Go to "OAuth consent screen" in Google Cloud Console 38 | - Choose "External" user type 39 | - Fill in the required information 40 | - Add your email as a test user 41 | - Add the scope: `https://www.googleapis.com/auth/youtube` 42 | 43 | ## Usage 44 | 45 | 1. Migrate a Spotify playlist (basic usage): 46 | ```bash 47 | python spotify2yt.py "https://open.spotify.com/playlist/YOUR_PLAYLIST_ID" 48 | ``` 49 | 50 | 2. Migrate liked songs: 51 | ```bash 52 | python spotify2yt.py --liked-songs 53 | ``` 54 | 55 | 3. Migrate liked songs with custom name: 56 | ```bash 57 | python spotify2yt.py --liked-songs --name "My Liked Songs" 58 | ``` 59 | 60 | 4. Migrate a playlist with custom name: 61 | ```bash 62 | python spotify2yt.py "https://open.spotify.com/playlist/YOUR_PLAYLIST_ID" --name "My Custom Playlist" 63 | ``` 64 | 65 | 5. With custom name and description: 66 | ```bash 67 | python spotify2yt.py "https://open.spotify.com/playlist/YOUR_PLAYLIST_ID" --name "My Custom Playlist" --description "My favorite songs" 68 | ``` 69 | 70 | Command-line Arguments: 71 | - `spotify_url` (optional): The URL of your Spotify playlist (required if not using --liked-songs) 72 | - `--liked-songs`: Flag to migrate liked songs instead of a playlist 73 | - `--name`: Custom name for the YouTube playlist (default: "Spotify Playlist" or "Liked Songs") 74 | - `--description`: Custom description for the YouTube playlist (default: "Migrated from Spotify") 75 | 76 | The script will: 77 | - Open your browser for YouTube authentication 78 | - Fetch tracks from your Spotify playlist or liked songs 79 | - Create a new private YouTube playlist 80 | - Search for each song on YouTube 81 | - Add the found videos to your playlist 82 | 83 | ## Features 84 | 85 | - Migrates entire Spotify playlists to YouTube 86 | - Migrates liked songs to YouTube 87 | - Maintains song order 88 | - Handles rate limiting 89 | - Creates private playlists by default 90 | - Batch processing to avoid API limits 91 | - Progress tracking 92 | - Error handling 93 | - Custom playlist naming and description 94 | - Support for Spotify playlist URLs 95 | 96 | ## Notes 97 | 98 | - The script creates a `token.pickle` file to store YouTube credentials 99 | - YouTube playlists are created as private by default 100 | - The script uses the first search result for each song 101 | - Rate limiting is implemented to avoid API quota issues 102 | - When migrating liked songs, the playlist will be named "Liked Songs" by default 103 | 104 | ## Troubleshooting 105 | 106 | 1. If you get "redirect_uri_mismatch" error: 107 | - Add `http://localhost:8080/` to authorized redirect URIs in Google Cloud Console 108 | 109 | 2. If you get "access_denied" error: 110 | - Make sure you've added your email as a test user in OAuth consent screen 111 | - Verify that you've added the YouTube scope 112 | 113 | 3. If videos aren't being found: 114 | - The search might not find exact matches 115 | - Try modifying the search query format in the code 116 | 117 | ## License 118 | 119 | This project is licensed under the MIT License - see the LICENSE file for details. -------------------------------------------------------------------------------- /yt2spotify.py: -------------------------------------------------------------------------------- 1 | import yt_dlp 2 | import re 3 | import spotipy 4 | from spotipy.oauth2 import SpotifyOAuth 5 | import os 6 | from dotenv import load_dotenv 7 | import argparse 8 | import pickle 9 | import time 10 | 11 | load_dotenv() 12 | 13 | def get_playlist_titles(playlist_url): 14 | ydl_opts = { 15 | 'quiet': True, 16 | 'extract_flat': True, 17 | 'ignoreerrors': True, 18 | } 19 | titles = [] 20 | with yt_dlp.YoutubeDL(ydl_opts) as ydl: 21 | try: 22 | playlist_info = ydl.extract_info(playlist_url, download=False) 23 | if not playlist_info: 24 | print("Could not extract playlist information") 25 | return [] 26 | for entry in playlist_info['entries']: 27 | if entry: 28 | titles.append(entry.get('title', 'Unknown')) 29 | return titles 30 | except Exception as e: 31 | print(f"Error: {str(e)}") 32 | return [] 33 | 34 | def extract_song_name(title): 35 | # Remove common YouTube title additions 36 | title = re.sub(r"\[.*?\]|\(.*?\)", "", title) 37 | 38 | # Split on common delimiters, but keep both parts 39 | parts = re.split(r"[-:–—|]", title, maxsplit=1) 40 | 41 | # Clean up each part 42 | cleaned_parts = [] 43 | for part in parts: 44 | # Remove common YouTube video indicators 45 | part = re.sub(r"\b(official|video|lyrics?|audio|remastered|live|hd|hq|full album|feat\.?|ft\.?)\b", "", part, flags=re.I) 46 | # Remove leading/trailing non-alphanumeric characters 47 | part = re.sub(r"^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$", "", part) 48 | # Normalize whitespace 49 | part = re.sub(r"\s+", " ", part) 50 | cleaned_parts.append(part.strip()) 51 | 52 | # If we have two parts, assume first is artist and second is song 53 | if len(cleaned_parts) > 1: 54 | artist = cleaned_parts[0] 55 | song = cleaned_parts[1] 56 | return f"{artist} - {song}" 57 | else: 58 | return cleaned_parts[0] 59 | 60 | SPOTIPY_CLIENT_ID = os.getenv('SPOTIPY_CLIENT_ID') 61 | SPOTIPY_CLIENT_SECRET = os.getenv('SPOTIPY_CLIENT_SECRET') 62 | SPOTIPY_REDIRECT_URI = os.getenv('SPOTIPY_REDIRECT_URI') 63 | SCOPE = 'playlist-modify-public playlist-modify-private' 64 | 65 | sp = spotipy.Spotify(auth_manager=SpotifyOAuth( 66 | client_id=SPOTIPY_CLIENT_ID, 67 | client_secret=SPOTIPY_CLIENT_SECRET, 68 | redirect_uri=SPOTIPY_REDIRECT_URI, 69 | scope=SCOPE 70 | )) 71 | user_id = sp.current_user()['id'] 72 | 73 | def get_or_create_playlist(playlist_name, delete_existing=False): 74 | playlists = sp.current_user_playlists(limit=50) 75 | for playlist in playlists['items']: 76 | if playlist['name'].lower() == playlist_name.lower(): 77 | if delete_existing: 78 | sp.user_playlist_unfollow(user=user_id, playlist_id=playlist['id']) 79 | print(f"Deleted playlist: {playlist_name}") 80 | else: 81 | response = input(f"Playlist '{playlist_name}' already exists. Do you want to delete it? (y/n): ") 82 | if response.lower() == 'y': 83 | sp.user_playlist_unfollow(user=user_id, playlist_id=playlist['id']) 84 | print(f"Deleted playlist: {playlist_name}") 85 | else: 86 | print("Using existing playlist.") 87 | return playlist['id'] 88 | new_playlist = sp.user_playlist_create(user=user_id, name=playlist_name, public=False) 89 | print(f"Created playlist: {playlist_name}") 90 | return new_playlist['id'] 91 | 92 | 93 | def search_track_uris(song_names, playlist_id): 94 | uris = [] 95 | existing_tracks = set() 96 | 97 | results = sp.playlist_tracks(playlist_id) 98 | for item in results.get('items', []): 99 | if item.get('track'): 100 | existing_tracks.add(item['track']['uri']) 101 | for name in song_names: 102 | if not name.strip(): 103 | continue 104 | max_retries = 3 105 | for attempt in range(max_retries): 106 | try: 107 | results = sp.search(q=name, type='track', limit=1) 108 | tracks = results.get('tracks', {}).get('items', []) 109 | if tracks: 110 | track_uri = tracks[0]['uri'] 111 | if track_uri not in existing_tracks: 112 | uris.append(track_uri) 113 | print(f"Found: {name} → {tracks[0]['name']} by {tracks[0]['artists'][0]['name']}") 114 | else: 115 | print(f"Already in playlist: {name}") 116 | else: 117 | print(f"Not found: {name}") 118 | break 119 | except Exception as e: 120 | if attempt < max_retries - 1: 121 | print(f"Retrying search for {name} due to error: {str(e)}") 122 | time.sleep(2) # Wait before retrying 123 | else: 124 | print(f"Failed to search for {name} after {max_retries} attempts: {str(e)}") 125 | return uris 126 | 127 | if __name__ == "__main__": 128 | parser = argparse.ArgumentParser(description='Transfer songs from a YouTube playlist to a Spotify playlist.') 129 | parser.add_argument('--playlist_url', type=str, default='https://www.youtube.com/playlist?list=PLo2RLtsf-Zl3r5KoTWIzfFudBx_v0BZb6', 130 | help='URL of the YouTube playlist to process') 131 | parser.add_argument('--playlist_name', type=str, default='youtube', 132 | help='Name of the Spotify playlist to create or use') 133 | parser.add_argument('--delete', action='store_true', 134 | help='Delete all existing playlists with the same name without prompting') 135 | args = parser.parse_args() 136 | print(args.playlist_url) 137 | # Extract playlist ID from the URL 138 | playlist_id_tosave = args.playlist_url.split('list=')[-1] 139 | pickle_file = f'track_uris_{playlist_id_tosave}.pkl' 140 | playlist_id = get_or_create_playlist(args.playlist_name, args.delete) 141 | 142 | if os.path.exists(pickle_file): 143 | print(f"Loading existing track URIs from {pickle_file}.") 144 | with open(pickle_file, 'rb') as f: 145 | track_uris = pickle.load(f) 146 | else: 147 | print("No existing track URIs found. Searching for new tracks...") 148 | titles = get_playlist_titles(args.playlist_url) 149 | purged_titles = [extract_song_name(t) for t in titles] 150 | print(f"Purged song names: {purged_titles[:10]} ... (total: {len(purged_titles)})") 151 | track_uris = search_track_uris(purged_titles, playlist_id) 152 | with open(pickle_file, 'wb') as f: 153 | pickle.dump(track_uris, f) 154 | 155 | print(f"Found {len(track_uris)} tracks") 156 | if track_uris: 157 | # Add tracks in batches of 100 158 | batch_size = 100 159 | for i in range(0, len(track_uris), batch_size): 160 | batch = track_uris[i:i + batch_size] 161 | try: 162 | sp.playlist_add_items(playlist_id=playlist_id, items=batch) 163 | print(f"Added tracks {i + 1} to {i + len(batch)} to playlist.") 164 | except Exception as e: 165 | print(f"Error adding tracks {i + 1} to {i + len(batch)}: {str(e)}") 166 | print("Track URIs saved to 'track_uris.pkl'. You can continue from here on the next run.") 167 | else: 168 | print("No valid tracks found to add.") -------------------------------------------------------------------------------- /spotify2yt.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import spotipy 4 | from spotipy.oauth2 import SpotifyOAuth 5 | from googleapiclient.discovery import build 6 | from google_auth_oauthlib.flow import InstalledAppFlow 7 | from google.auth.transport.requests import Request 8 | import pickle 9 | import argparse 10 | from dotenv import load_dotenv 11 | import time 12 | import re 13 | 14 | # Set console encoding to UTF-8 15 | if sys.platform == 'win32': 16 | sys.stdout.reconfigure(encoding='utf-8') 17 | sys.stderr.reconfigure(encoding='utf-8') 18 | 19 | load_dotenv() 20 | 21 | # Spotify setup 22 | SPOTIPY_CLIENT_ID = os.getenv('SPOTIPY_CLIENT_ID') 23 | SPOTIPY_CLIENT_SECRET = os.getenv('SPOTIPY_CLIENT_SECRET') 24 | SPOTIPY_REDIRECT_URI = os.getenv('SPOTIPY_REDIRECT_URI') 25 | SCOPE = 'playlist-read-private playlist-read-collaborative user-library-read' 26 | 27 | # YouTube API setup 28 | YOUTUBE_API_SERVICE_NAME = "youtube" 29 | YOUTUBE_API_VERSION = "v3" 30 | SCOPES = ['https://www.googleapis.com/auth/youtube'] 31 | 32 | # Initialize Spotify client 33 | sp = spotipy.Spotify(auth_manager=SpotifyOAuth( 34 | client_id=SPOTIPY_CLIENT_ID, 35 | client_secret=SPOTIPY_CLIENT_SECRET, 36 | redirect_uri=SPOTIPY_REDIRECT_URI, 37 | scope=SCOPE 38 | )) 39 | 40 | def get_youtube_service(): 41 | """Set up YouTube API service.""" 42 | credentials = None 43 | 44 | # Load credentials from token.pickle if it exists 45 | if os.path.exists('token.pickle'): 46 | with open('token.pickle', 'rb') as token: 47 | credentials = pickle.load(token) 48 | 49 | # If credentials don't exist or are invalid, get new ones 50 | if not credentials or not credentials.valid: 51 | if credentials and credentials.expired and credentials.refresh_token: 52 | credentials.refresh(Request()) 53 | else: 54 | flow = InstalledAppFlow.from_client_secrets_file( 55 | 'client_secrets.json', 56 | SCOPES, 57 | redirect_uri='http://localhost:8080/' 58 | ) 59 | credentials = flow.run_local_server(port=8080) 60 | 61 | # Save credentials for future use 62 | with open('token.pickle', 'wb') as token: 63 | pickle.dump(credentials, token) 64 | 65 | return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, credentials=credentials) 66 | 67 | def get_spotify_playlist_tracks(playlist_id): 68 | """Get all tracks from a Spotify playlist.""" 69 | tracks = [] 70 | results = sp.playlist_tracks(playlist_id) 71 | tracks.extend(results['items']) 72 | 73 | while results['next']: 74 | results = sp.next(results) 75 | tracks.extend(results['items']) 76 | 77 | return tracks 78 | 79 | def get_spotify_liked_tracks(): 80 | """Get all liked tracks from Spotify.""" 81 | tracks = [] 82 | results = sp.current_user_saved_tracks() 83 | tracks.extend(results['items']) 84 | 85 | while results['next']: 86 | results = sp.next(results) 87 | tracks.extend(results['items']) 88 | 89 | return tracks 90 | 91 | def search_youtube_video(youtube, query): 92 | """Search for a video on YouTube.""" 93 | try: 94 | request = youtube.search().list( 95 | part="snippet", 96 | q=query, 97 | type="video", 98 | maxResults=1 99 | ) 100 | response = request.execute() 101 | 102 | if response['items']: 103 | return response['items'][0]['id']['videoId'] 104 | return None 105 | except Exception as e: 106 | print(f"Error searching YouTube: {str(e)}") 107 | return None 108 | 109 | def create_youtube_playlist(youtube, title, description=""): 110 | """Create a new YouTube playlist.""" 111 | try: 112 | request = youtube.playlists().insert( 113 | part="snippet,status", 114 | body={ 115 | "snippet": { 116 | "title": title, 117 | "description": description 118 | }, 119 | "status": { 120 | "privacyStatus": "private" 121 | } 122 | } 123 | ) 124 | response = request.execute() 125 | return response['id'] 126 | except Exception as e: 127 | print(f"Error creating YouTube playlist: {str(e)}") 128 | return None 129 | 130 | def add_to_youtube_playlist(youtube, playlist_id, video_ids): 131 | """Add videos to a YouTube playlist.""" 132 | try: 133 | for video_id in video_ids: 134 | request = youtube.playlistItems().insert( 135 | part="snippet", 136 | body={ 137 | "snippet": { 138 | "playlistId": playlist_id, 139 | "resourceId": { 140 | "kind": "youtube#video", 141 | "videoId": video_id 142 | } 143 | } 144 | } 145 | ) 146 | request.execute() 147 | return True 148 | except Exception as e: 149 | print(f"Error adding videos to playlist: {str(e)}") 150 | return False 151 | 152 | def extract_playlist_id(playlist_url): 153 | """Extract playlist ID from Spotify URL.""" 154 | # Match pattern: /playlist/{id} or playlist/{id} 155 | pattern = r'playlist/([a-zA-Z0-9]+)' 156 | match = re.search(pattern, playlist_url) 157 | if match: 158 | return match.group(1) 159 | return None 160 | 161 | def main(): 162 | # Set up argument parser 163 | parser = argparse.ArgumentParser(description='Migrate Spotify playlist to YouTube') 164 | parser.add_argument('spotify_url', nargs='?', help='Spotify playlist URL (required if not using --liked-songs)') 165 | parser.add_argument('--name', help='YouTube playlist name (default: Spotify Playlist)', default='Spotify Playlist') 166 | parser.add_argument('--description', help='YouTube playlist description', default='Migrated from Spotify') 167 | parser.add_argument('--liked-songs', action='store_true', help='Migrate liked songs instead of a playlist') 168 | args = parser.parse_args() 169 | 170 | # Validate arguments 171 | if not args.liked_songs and not args.spotify_url: 172 | print("Error: Either provide a Spotify playlist URL or use --liked-songs flag") 173 | return 174 | 175 | # Set up YouTube API 176 | print("Setting up YouTube API...") 177 | youtube = get_youtube_service() 178 | if not youtube: 179 | print("Failed to set up YouTube API. Exiting.") 180 | return 181 | 182 | # Get tracks based on mode 183 | if args.liked_songs: 184 | print("Fetching liked tracks from Spotify...") 185 | tracks = get_spotify_liked_tracks() 186 | if not args.name: 187 | args.name = "Liked Songs" 188 | if not args.description: 189 | args.description = "Migrated from Spotify Liked Songs" 190 | else: 191 | # Extract playlist ID from URL 192 | playlist_id = extract_playlist_id(args.spotify_url) 193 | if not playlist_id: 194 | print("Invalid Spotify playlist URL. Please provide a valid URL.") 195 | return 196 | print(f"Fetching tracks from Spotify playlist...") 197 | tracks = get_spotify_playlist_tracks(playlist_id) 198 | 199 | if not tracks: 200 | print("No tracks found.") 201 | return 202 | 203 | # Create YouTube playlist 204 | print(f"Creating YouTube playlist: {args.name}") 205 | youtube_playlist_id = create_youtube_playlist(youtube, args.name, args.description) 206 | 207 | if not youtube_playlist_id: 208 | print("Failed to create YouTube playlist.") 209 | return 210 | 211 | # Search and add tracks 212 | video_ids = [] 213 | for i, item in enumerate(tracks, 1): 214 | # Get track object based on mode 215 | if args.liked_songs: 216 | track = item['track'] # Liked songs have the track in a nested 'track' field 217 | else: 218 | track = item['track'] # Playlist items have the track in a nested 'track' field 219 | 220 | if not track: 221 | continue 222 | 223 | # Create search query from track info 224 | artist = track['artists'][0]['name'] 225 | title = track['name'] 226 | query = f"{artist} - {title}" 227 | 228 | print(f"[{i}/{len(tracks)}] Searching for: {query}") 229 | video_id = search_youtube_video(youtube, query) 230 | 231 | if video_id: 232 | video_ids.append(video_id) 233 | print(f"Found: {query}") 234 | else: 235 | print(f"Not found: {query}") 236 | 237 | # Add tracks in batches of 50 to avoid rate limiting 238 | if len(video_ids) >= 50: 239 | print(f"Adding batch of {len(video_ids)} tracks to playlist...") 240 | add_to_youtube_playlist(youtube, youtube_playlist_id, video_ids) 241 | video_ids = [] 242 | time.sleep(2) # Rate limiting precaution 243 | 244 | # Add any remaining tracks 245 | if video_ids: 246 | print(f"Adding final batch of {len(video_ids)} tracks to playlist...") 247 | add_to_youtube_playlist(youtube, youtube_playlist_id, video_ids) 248 | 249 | print("Migration completed!") 250 | 251 | if __name__ == "__main__": 252 | main() --------------------------------------------------------------------------------