Узнайте, как написать Python-скрипт для синхронизации плейлистов YouTube и Spotify. Разбор кода построчно, от .env до финального запуска.
Вы уже получили ключи API для YouTube и Spotify. Теперь пришло время собрать из них рабочий скрипт, который автоматически синхронизирует плейлисты. К концу этой главы вы будете понимать каждую строку кода — зачем она нужна и как работает. Никаких пропущенных знаний: если что-то останется непонятным, пишите в комментарии, я исправлю объяснение.
Проект состоит из трёх файлов и одного конфигурационного файла:
youtube-to-spotify/
├── spotify.py # знает, как общаться со Spotify
├── youtube.py # знает, как общаться с YouTube
├── script.py # мозг — запускает синхронизацию
└── .env # ваши приватные ключи (никому не показывать)
Мы разберём каждый файл по порядку.
Это не код, а просто текстовый файл, который хранит шесть ключей из предыдущих глав. Создайте в папке проекта файл .env и заполните его:
CLIENT_ID=your_spotify_client_id
CLIENT_SECRET=your_spotify_client_secret
REFRESH_TOKEN=your_spotify_refresh_token
SPOTIFY_PLAYLIST_ID=your_spotify_playlist_id
API_KEY=your_youtube_api_key
YOUTUBE_PLAYLIST_ID=PLGBuKfnErZlAkaUUy57-mR97f8SBgMNHh
Последняя строка уже заполнена — это плейлист 70-х, который мы используем. Остальные замените на свои значения из предыдущих глав. Этот файл остаётся только на вашем компьютере, он никогда не попадает на GitHub (мы проверим это в главе 6).
Этот файл отвечает за общение с YouTube. У него три задачи.
def get_playlist_items(self, playlist_id):
request = self.youtube.playlistItems().list(
part='snippet',
playlistId=playlist_id,
maxResults=50
)
videos = []
while request:
response = request.execute()
for item in response['items']:
title = item['snippet']['title']
video_id = item['snippet']['resourceId']['videoId']
videos.append((title, f"https://www.youtube.com/watch?v={video_id}"))
request = self.youtube.playlistItems().list_next(request, response)
return videos
API YouTube отдаёт только 50 видео за раз. В нашем плейлисте 150 песен, поэтому цикл while request продолжает запрашивать следующие 50, пока не получит все. В итоге videos — это список из 150 пар (название, ссылка). Пример:
("Stayin' Alive - Bee Gees (Official Music Video)", "https://www.youtube.com/watch?v=I_izvAbhExY")
Названия видео на YouTube — настоящий хаос. Каждый автор оформляет их по-своему:
Stayin' Alive - Bee Gees (Official Music Video)
Bee Gees - Stayin' Alive | Lyrics
ABBA - Dancing Queen (Official)
Don't Stop Me Now [Queen] HD
Единого правила нет. Поэтому мы пытаемся их осмыслить:
def extract_song_and_artist(self, title):
if " - " in title:
parts = title.split(" - ")
if len(parts) == 2:
artist_first = parts[0].strip()
song_first = parts[1].strip()
song_second = parts[0].strip()
artist_second = parts[1].strip()
return (song_first, artist_first), (song_second, artist_second)
return (title, ""), (title, "")
Когда в названии есть дефис, мы разделяем строку и возвращаем оба возможных варианта: «Исполнитель — Песня» и «Песня — Исполнитель». Позже основной скрипт пробует оба на Spotify и выбирает тот, который дал совпадение. Если дефиса нет, считаем всё название названием песни и ищем без исполнителя — часто такой широкий поиск всё равно срабатывает.
Перед поиском на Spotify мы удаляем весь мусор YouTube — «(Official Music Video)», «[HD]», «ft.», «#lyrics» и ещё около 40 вариаций, которые сбивают поиск:
def clean_title(self, title):
title = re.sub(r'\(.*?\)|\[.*?\]|\{.*?\}|#\S+', '', title)
# ... удаляет "Official Video", "Lyrics", "HD" и т.д.
title = re.sub(r'\s+', ' ', title).strip()
return title
Так «Stayin' Alive — Bee Gees (Official Music Video)» превращается в «Stayin Alive — Bee Gees» перед отправкой на Spotify. Гораздо чище для поиска.
Этот файл управляет всем, что связано со Spotify. Четыре функции наиболее важны для синхронизации.
def refresh_access_token(self):
# Использует CLIENT_ID, CLIENT_SECRET и REFRESH_TOKEN
# чтобы получить свежий access token при каждом запуске скрипта
Токены доступа Spotify истекают через час. Refresh Token, полученный в главе 2, позволяет автоматически генерировать новый токен при каждом запуске скрипта. Благодаря этому скрипт может работать по расписанию без вашего участия — вам больше никогда не нужно входить в систему.
def search_song(self, query, limit=10):
endpoint = f'search?q={query}&type=track&limit=10'
# ... запрашивает у Spotify 10 лучших результатов
# ... затем выполняет три прохода на совпадение
Эта функция не просто берёт первый результат и надеется на лучшее. Она делает три прохода:
Если все три прохода не дали результата, функция возвращает None, и скрипт логирует песню как ненайденную.
def add_tracks_to_playlist(self, playlist_id, track_uris):
# Добавляет треки порциями по 100 (лимит API Spotify)
def remove_tracks_from_playlist(self, playlist_id, track_uris):
# Удаляет треки порциями по 100
API Spotify принимает не более 100 треков за один запрос, поэтому обе функции автоматически разбивают большие списки на порции. Вам не нужно думать об этом — всё работает само.
def reorder_playlist_tracks(self, playlist_id, track_uris):
# Перемещает треки один за другим, пока порядок не совпадёт с YouTube
def reorder_playlist_many_tracks(self, playlist_id, track_uris):
# Более эффективная версия для плейлистов длиннее 100 треков
Поскольку в нашем плейлисте 150 песен, скрипт автоматически использует reorder_playlist_many_tracks. После каждой синхронизации порядок в вашем плейлисте Spotify будет точно соответствовать порядку в плейлисте YouTube.
Это файл, который вы запускаете. Он загружает ключи, подключается к обоим API и вызывает функцию синхронизации.
def main():
load_dotenv() # читает ваш .env файл
client_id = os.getenv('CLIENT_ID')
client_secret = os.getenv('CLIENT_SECRET')
# ... загружает все шесть значений
spotify = SpotifyAPI(client_id, client_secret, refresh_token)
youtube = YouTubeAPI(api_key)
sync_playlist(spotify_playlist_id, youtube_playlist_id, spotify, youtube)
load_dotenv() читает файл .env и делает его значения доступными как переменные окружения. os.getenv('CLIENT_ID') забирает их. Затем скрипт проверяет, что ни одно из шести значений не пропущено — если вы забыли заполнить что-то, он скажет, какое именно, вместо того чтобы упасть с непонятной ошибкой.
def sync_playlist(spotify_playlist_id, youtube_playlist_id, spotify, youtube, remove=True):
Шаг 1 — получить видео с YouTube
youtube_videos = youtube.get_playlist_items(youtube_playlist_id)
Получает все 150 видео из нашего плейлиста 70-х вместе с названиями.
Шаг 2 — найти каждое на Spotify
for i, (title, url) in enumerate(youtube_videos, start=1):
(song_a, artist_a), (song_b, artist_b) = youtube.extract_song_and_artist(title)
query_a = f"{youtube.clean_title(song_a)} {youtube.clean_title(artist_a)}".strip()
query_b = f"{youtube.clean_title(song_b)} {youtube.clean_title(artist_b)}".strip()
result = spotify.search_song(query_a) or spotify.search_song(query_b)
Для каждого видео мы строим два запроса (оба возможных порядка «Исполнитель/Песня»), очищаем их и пробуем на Spotify. or означает: сначала пробуем query_a, и только если он ничего не дал, пробуем query_b.
Шаг 3 — попробовать альтернативы из скобок
for alt in (youtube.extract_bracket_alternatives(song_a) + ...):
result = spotify.search_song(alt_query)
Некоторые видео прячут альтернативное название песни в скобках: «Stayin' Alive (From Saturday Night Fever Soundtrack)». Если основные запросы не сработали, мы пробуем искать по тексту внутри скобок как последний шанс.
Шаг 4 — добавить, удалить, переупорядочить
to_add = [uri for uri in found_tracks if uri not in spotify_uris]
spotify.add_tracks_to_playlist(spotify_playlist_id, to_add)
to_remove = [uri for uri in spotify_uris if uri not in found_tracks]
spotify.remove_tracks_from_playlist(spotify_playlist_id, to_remove)
spotify.reorder_playlist_many_tracks(spotify_playlist_id, found_tracks)
Три сравнения списков:
Шаг 5 — вывести итог
print(f"YouTube videos: {len(youtube_videos)}")
print(f"Found on Spotify: {len(found_tracks)}")
print(f"Not found: {len(not_found)}")
print(f"Added: {len(to_add)}")
print(f"Removed: {len(to_remove)}")
Когда скрипт завершается, вы получаете чистую сводку всего, что было сделано. При первом запуске число «Added» будет большим. При каждом последующем — только изменения с момента последней синхронизации.
Вы можете спросить, почему бы не поместить всё в один большой script.py. Краткий ответ: разделение ответственности — каждый файл знает о чём-то одном. youtube.py знает YouTube. spotify.py знает Spotify. script.py знает логику. Если Spotify завтра изменит свой API, вы правите только spotify.py. Если захотите заменить YouTube на другой источник музыки, меняете только youtube.py. Логика в script.py не должна знать и не знает об этих деталях. Так устроено настоящее программное обеспечение. И вы только что это сделали.
Сейчас у вас есть полное понимание кода. В следующей главе мы установим Python, настроим проект на вашем компьютере и впервые запустим скрипт. Вы увидите, как он вытащит все 150 песен из плейлиста 70-х и на лету соберёт ваш плейлист Spotify. А пока — сохраните этот код, он вам понадобится.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →