TypeScript로 Spotify에서 YouTube Music 재생목록 전송 자동화

발행: (2025년 12월 20일 오후 11:45 GMT+9)
10 min read
원문: Dev.to

Source: Dev.to

번역할 전체 텍스트를 제공해 주시면, 요청하신 대로 마크다운 형식과 코드 블록, URL은 그대로 유지하면서 한국어로 번역해 드리겠습니다.

📋 Overview

음악 스트리밍 서비스 간 전환이 신중하게 큐레이션한 재생 목록을 잃는 것을 의미해서는 안 됩니다. 영구적으로 마이그레이션하든 두 플랫폼 모두에서 재생 목록을 유지하든, 작은 TypeScript 스크립트가 여러분을 대신해 무거운 작업을 수행할 수 있습니다.

🛠 전제 조건

RequirementDetails
Node.jsv18 또는 그 이상
Spotify Developer앱이 생성된 계정 (클라이언트 ID, 클라이언트 시크릿, 리디렉션 URI)
YouTube Music일반 계정 (인증 쿠키가 필요합니다)
TypeScript언어에 대한 기본 지식
Environment variables비밀 정보를 위한 .env 파일

🚀 프로젝트 설정

# Create a new folder and initialise the project
mkdir spotify-to-ytmusic
cd spotify-to-ytmusic
npm init -y

# Development dependencies
npm install typescript ts-node @types/node --save-dev

# Runtime dependencies
npm install @spotify/web-api-ts-sdk ytmusic-api youtube-music-ts-api dotenv

# Initialise a tsconfig.json
npx tsc --init

.env

프로젝트 루트에 .env 파일을 만들고 아래 내용을 붙여넣으세요 (플레이스홀더를 자신의 값으로 교체하세요):

SPOTIFY_CLIENT_ID=your_client_id
SPOTIFY_CLIENT_SECRET=your_client_secret
SPOTIFY_REDIRECT_URI=http://localhost:3000/callback

🎧 Spotify – 재생목록 트랙 가져오기

// src/spotify.ts
import { SpotifyApi } from "@spotify/web-api-ts-sdk";

export interface Track {
  name: string;
  artist: string;
  album: string;
}

/**
 * Spotify 재생목록에서 모든 트랙을 가져옵니다.
 */
export async function getSpotifyPlaylistTracks(
  playlistId: string
): Promise<Track[]> {
  const sdk = SpotifyApi.withClientCredentials(
    process.env.SPOTIFY_CLIENT_ID!,
    process.env.SPOTIFY_CLIENT_SECRET!
  );

  const tracks: Track[] = [];
  let offset = 0;
  const limit = 50;

  while (true) {
    const response = await sdk.playlists.getPlaylistItems(
      playlistId,
      undefined,
      undefined,
      limit,
      offset
    );

    for (const item of response.items) {
      if (item.track && "name" in item.track) {
        tracks.push({
          name: item.track.name,
          artist: item.track.artists[0]?.name ?? "Unknown",
          album: item.track.album?.name ?? "Unknown",
        });
      }
    }

    if (response.items.length < limit) {
      break;
    }
    offset += limit;
  }

  return tracks;
}

🔎 YouTube Music 검색

// src/ytmusic.ts
import YTMusic from "ytmusic-api";

export interface YTMusicTrack {
  videoId: string;
  name: string;
  artist: string;
}

/**
 * Searches YouTube Music for a track and returns the first match.
 */
export async function searchYouTubeMusic(
  ytmusic: YTMusic,
  track: { name: string; artist: string }
): Promise<YTMusicTrack | null> {
  const query = `${track.name} ${track.artist}`;

  try {
    const results = await ytmusic.searchSongs(query);
    if (results.length > 0) {
      const match = results[0];
      return {
        videoId: match.videoId,
        name: match.name,
        artist: match.artist?.name ?? "Unknown",
      };
    }
  } catch (error) {
    console.error(`Failed to find: ${query}`, error);
  }

  return null;
}

🔄 전송 함수 – 모든 것을 연결하기

// src/transfer.ts
import YTMusic from "ytmusic-api";
import { getSpotifyPlaylistTracks, Track } from "./spotify";
import { searchYouTubeMusic, YTMusicTrack } from "./ytmusic";

export async function transferPlaylist(
  spotifyPlaylistId: string,
  newPlaylistName: string
): Promise<void> {
  console.log("🎵 Starting playlist transfer...");

  // Initialise YouTube Music API
  const ytmusic = new YTMusic();
  await ytmusic.initialize();

  // 1️⃣ Fetch Spotify tracks
  console.log("📥 Fetching Spotify playlist tracks...");
  const spotifyTracks = await getSpotifyPlaylistTracks(spotifyPlaylistId);
  console.log(`Found ${spotifyTracks.length} tracks`);

  // 2️⃣ Search for each track on YouTube Music
  console.log("🔍 Searching for matches on YouTube Music...");
  const matchedTracks: YTMusicTrack[] = [];
  const notFound: string[] = [];

  for (const track of spotifyTracks) {
    const match = await searchYouTubeMusic(ytmusic, track);
    if (match) {
      matchedTracks.push(match);
      console.log(`✓ Found: ${track.name} – ${track.artist}`);
    } else {
      notFound.push(`${track.name} – ${track.artist}`);
      console.log(`✗ Not found: ${track.name} – ${track.artist}`);
    }

    // Simple rate‑limiting to avoid throttling
    await new Promise((r) => setTimeout(r, 500));
  }

  // 3️⃣ Summary
  console.log("\n📊 Transfer Summary:");
  console.log(`✓ Matched:   ${matchedTracks.length}`);
  console.log(`✗ Not found: ${notFound.length}`);

  if (notFound.length) {
    console.log("\nTracks not found:");
    notFound.forEach((t) => console.log(`  - ${t}`));
  }

  // 4️⃣ (Optional) Create a YouTube Music playlist and add the matches
  // await createYTMusicPlaylist(...);
}

Source:

📂 YouTube Music 재생목록 만들기 (쿠키 기반 인증)

// src/ytmusic-create.ts
import YouTubeMusic from "youtube-music-ts-api";

export async function createYTMusicPlaylist(
  cookieString: string,
  playlistName: string,
  videoIds: string[]
): Promise<void> {
  const ytm = new YouTubeMusic();
  const ytma = await ytm.authenticate(cookieString);

  // Create the new playlist
  const playlist = await ytma.createPlaylist(
    playlistName,
    "Imported from Spotify",
    "PRIVATE"
  );

  console.log(`Created playlist: ${playlist.name}`);

  // Add tracks – you need full track objects, not just IDs.
  // Example (pseudo‑code):
  // for (const id of videoIds) {
  //   const track = await ytma.getTrack(id);
  //   await ytma.addTrackToPlaylist(playlist.id, track);
  // }
}

인증 쿠키를 얻는 방법

  1. 브라우저에서 YouTube Music을 엽니다.
  2. F12Network 탭을 누릅니다.
  3. 아무 작업(예: 노래 재생)을 수행합니다.
  4. music.youtube.com에 대한 요청을 찾아 Cookie 헤더 값을 복사합니다.
  5. 해당 문자열을 스크립트에 붙여넣거나 환경 변수에 저장합니다.

📂 전체 index.ts 예시

// src/index.ts
import "dotenv/config";
import { transferPlaylist } from "./transfer";

async function main() {
  // Spotify playlist ID (taken from the URL)
  const spotifyPlaylistId = "37i9dQZF1DXcBWIGoYBM5M"; // Today's Top Hits

  // Desired name for the new YouTube Music playlist
  const ytPlaylistName = "Today's Top Hits (from Spotify)";

  await transferPlaylist(spotifyPlaylistId, ytPlaylistName);
}

main().catch((err) => {
  console.error("❌ Unexpected error:", err);
  process.exit(1);
});

Note: 위 스니펫은 검색 및 매칭 단계만 수행합니다.
실제로 YouTube Music 재생목록을 만들고 트랙을 추가하려면 매칭 단계가 끝난 후 createYTMusicPlaylist(이전 섹션 참고)를 호출하고, 쿠키 문자열과 matchedTracks에서 얻은 videoId 배열을 전달하세요.

📦 프로젝트 실행

# Compile & run with ts-node (development)
npx ts-node src/index.ts

# Or compile first, then run the JavaScript output
npm run build   # assuming you added a "build" script that runs tsc
node dist/index.js

🧩 다음은?

  • 오류 처리 – 실패한 검색을 재시도하고, 퍼지 매칭으로 대체합니다.
  • 배치 처리 – 새 플레이리스트에 트랙을 대량으로 추가해 속도를 높입니다.
  • Spotify용 OAuth – 개인 플레이리스트를 읽거나 사용자의 라이브러리에 쓰기 위해 Authorization Code 흐름을 사용합니다.
  • CLI 래퍼 – 스크립트를 간단한 명령줄 도구(npx spotify-to-ytmusic …)로 노출합니다.

🎉 즐거운 코딩 되세요!

이제 몇 가지 명령만으로 Spotify 플레이리스트를 YouTube Music에 복사할 수 있는 완전한 TypeScript 기반 파이프라인이 준비되었습니다. 코드를 자유롭게 수정하고, 로깅을 추가하거나, 더 큰 자동화 스위트에 통합해 보세요.

import { YTMusic } from "ytmusic-api";
import SpotifyWebApi from "spotify-web-api-node";

async function main() {
  // -------------------------------------------------
  // 1️⃣  Get Spotify playlist tracks
  // -------------------------------------------------
  const spotify = new SpotifyWebApi({
    clientId: process.env.SPOTIFY_CLIENT_ID!,
    clientSecret: process.env.SPOTIFY_CLIENT_SECRET!,
    redirectUri: "http://localhost:8888/callback",
  });

  // Get an access token (you can also use a refresh token)
  const { body: tokenData } = await spotify.clientCredentialsGrant();
  spotify.setAccessToken(tokenData.access_token);

  // Replace with your playlist ID
  const playlistId = "YOUR_SPOTIFY_PLAYLIST_ID";

  const data = await spotify.getPlaylistTracks(playlistId, {
    fields: "items(track(name,artists(name)))",
    limit: 100,
  });

  const tracks = data.body.items.map((item) => ({
    name: item.track!.name,
    artist: (item.track as any).artists[0]?.name || "Unknown",
  }));

  console.log(`Found ${tracks.length} tracks`);

  // -------------------------------------------------
  // 2️⃣  Initialise YouTube Music client
  // -------------------------------------------------
  const ytmusic = new YTMusic();
  await ytmusic.initialize();

  // -------------------------------------------------
  // 3️⃣  Search & match each track (first 5 as demo)
  // -------------------------------------------------
  for (const track of tracks.slice(0, 5)) {
    const query = `${track.name} ${track.artist}`;
    const results = await ytmusic.searchSongs(query);

    if (results.length > 0) {
      console.log(`✓ ${track.name} → ${results[0].name}`);
    } else {
      console.log(`✗ ${track.name} – Not found`);
    }

    // Respect rate limits
    await new Promise((r) => setTimeout(r, 300));
  }
}

main().catch(console.error);

이제 완전한 TypeScript 기반 파이프라인을 통해 Spotify 플레이리스트를 YouTube Music에 손쉽게 복사할 수 있습니다. 코드를 자유롭게 조정하고, 로깅을 추가하거나, 더 큰 자동화 시스템에 통합해 보세요.

📦 준비된 솔루션

  • spotify-to-youtube – 트랙 매칭을 위한 NPM 패키지
  • SpotTransfer – 전체 마이그레이션 도구 (GUI)
  • spotify_to_ytmusic – 파이썬 CLI 도구

🛠️ 견고한 구현을 위한 팁

  • Rate limiting – API 호출 사이에 지연(300 ms 등)을 추가하여 스로틀링을 방지합니다.
  • Error handling – 일부 트랙은 정확히 일치하지 않을 수 있으므로 수동 검토를 위해 로그에 기록합니다.
  • Fuzzy matching – 매칭 정확도를 높이기 위해 퍼지 문자열 라이브러리(예: fuzzball)를 사용합니다.
  • Batch processing – 큰 플레이리스트의 경우 청크 단위로 처리하고 중간 결과를 지속합니다.
  • Logging – 나중에 수동으로 추가할 수 있도록 실패한 매치를 CSV/JSON 형태로 보관합니다.

🚀 스크립트 확장

  • Sync scheduling – 주기적인 업데이트를 위해 크론 작업으로 스크립트를 실행합니다.
  • Two‑way sync – 양쪽 플랫폼에서 추가/삭제를 감지하고 동기화 상태를 유지합니다.
  • Web UI – 간단한 프론트엔드를 위해 로직을 Express.js 앱으로 감쌉니다.
  • Discord bot – 슬래시 명령어를 통해 기능을 제공합니다.

전체 소스 코드(위 스크립트 포함)는 GitHub에서 확인할 수 있습니다. 즐거운 코딩 되세요!

📚 리소스

  • Spotify Web API TypeScript SDK
  • ytmusic-api
  • youtube-music-ts-api
  • Spotify Developer Documentation
Back to Blog

관련 글

더 보기 »

YINI Config Parser v1.3.2-beta 출시 (Node.js)

릴리스 개요 YINI용 공식 TypeScript 파서의 새로운 베타 버전이 출시되었습니다! YINI는 INI‑스타일의 가독성을 유지하면서 현대적이고 인간 친화적인 구성 포맷입니다…

celery-plus 🥬 — Node.js용 현대적인 Celery

왜 확인해 보세요? - 🚀 기존 Python Celery 워커와 함께 작동합니다 - 📘 TypeScript로 작성되었으며 전체 타입을 제공합니다 - 🔄 RabbitMQ AMQP와 Redis를 지원합니다 - ⚡ Async/a...

npx 이해하기: 실제 작동 방식

요약 이 기사에서는 npx를 두 단계로 설명합니다: - 정확한 해결 단계가 포함된 간략한 개요 - 각 단계에 대한 심층 설명 개요 npx는 exec를 검색합니다.