Automate Spotify to YouTube Music Playlist Transfer with TypeScript

Published: (December 20, 2025 at 09:45 AM EST)
7 min read
Source: Dev.to

Source: Dev.to

📋 Overview

Switching between music‑streaming services shouldn’t mean losing your carefully curated playlists. Whether you’re migrating permanently or maintaining playlists on both platforms, a small TypeScript script can do the heavy lifting for you.

🛠 Prerequisites

RequirementDetails
Node.jsv18 or newer
Spotify DeveloperAccount with an app created (client ID, client secret, redirect URI)
YouTube MusicA regular account (you’ll need its authentication cookie)
TypeScriptBasic knowledge of the language
Environment variables.env file for secrets

🚀 Project Setup

# 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

Create a file called .env in the project root and paste the following (replace the placeholders with your own values):

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

🎧 Spotify – Fetch Playlist Tracks

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

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

/**
 * Retrieves all tracks from a Spotify playlist.
 */
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;
}

🔎 Search 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;
}

🔄 Transfer Function – Glue Everything Together

// 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(...);
}

📂 Create a YouTube Music Playlist (Cookie‑Based Auth)

// 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. Open YouTube Music in your browser.
  2. Press F12Network tab.
  3. Perform any action (e.g., play a song).
  4. Locate a request to music.youtube.com and copy the Cookie header value.
  5. Paste that string into your script (or store it in an environment variable).

📂 Full index.ts Example

// 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: The snippet above only performs the search & matching step.
To actually create the YouTube Music playlist and add the tracks, call createYTMusicPlaylist (see the previous section) after the matching phase, passing the cookie string and the array of videoIds from matchedTracks.

📦 Running the Project

# 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

🧩 What’s Next?

  • Error handling – retry failed searches, fallback to fuzzy matching.
  • Batching – add tracks to the new playlist in bulk to speed things up.
  • OAuth for Spotify – use the Authorization Code flow if you need to read private playlists or write to a user’s library.
  • CLI wrapper – expose the script as a simple command‑line tool (npx spotify-to-ytmusic …).

🎉 Happy coding!

You now have a fully‑functional, TypeScript‑based pipeline that can copy any Spotify playlist to YouTube Music with just a few commands. Feel free to tweak the code, add more logging, or integrate it into a larger automation suite.

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);

📦 Ready‑made solutions

  • spotify-to-youtube – NPM package for matching tracks
  • SpotTransfer – Full migration tool (GUI)
  • spotify_to_ytmusic – Python CLI tool

🛠️ Tips for a robust implementation

  • Rate limiting – Add delays (e.g., 300 ms) between API calls to avoid throttling.
  • Error handling – Some tracks may not have exact matches; log these for manual review.
  • Fuzzy matching – Use a fuzzy‑string library (e.g., fuzzball) to improve match accuracy.
  • Batch processing – For large playlists, process in chunks and persist intermediate results.
  • Logging – Keep a CSV/JSON of failed matches for later manual addition.

🚀 Extending the script

  • Sync scheduling – Run the script on a cron job for periodic updates.
  • Two‑way sync – Detect additions/removals on either platform and keep them in sync.
  • Web UI – Wrap the logic in an Express.js app for a simple front‑end.
  • Discord bot – Expose the functionality via slash commands.

The full source code (including the above script) is available on GitHub. Happy coding!

📚 Resources

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

Related posts

Read more »

Understanding npx How It Really Works

Summary This article explains npx in two layers: - Brief overview with exact resolution steps - Deep explanation of each step Overview npx searches for an exec...