使用 TypeScript 自动化 Spotify 到 YouTube Music 播放列表转移
发布: (2025年12月20日 GMT+8 22:45)
8 min read
原文: Dev.to
Source: Dev.to
📋 概览
在音乐流媒体服务之间切换不应意味着失去您精心策划的播放列表。无论您是永久迁移还是在两个平台上维护播放列表,一个小型的 TypeScript 脚本都能为您完成繁重的工作。
🛠 前置条件
| 要求 | 细节 |
|---|---|
| Node.js | v18 或更高 |
| Spotify Developer | 已创建应用的账户(client ID、client secret、redirect URI) |
| YouTube Music | 普通账户(需要其认证 cookie) |
| 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;
}
/**
* 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;
}
🔎 搜索 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: …
📂 使用 Cookie 认证创建 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);
// 创建新播放列表
const playlist = await ytma.createPlaylist(
playlistName,
"Imported from Spotify",
"PRIVATE"
);
console.log(`Created playlist: ${playlist.name}`);
// 添加曲目 – 需要完整的曲目对象,而不仅仅是 ID。
// 示例(伪代码):
// for (const id of videoIds) {
// const track = await ytma.getTrack(id);
// await ytma.addTrackToPlaylist(playlist.id, track);
// }
}
获取认证 Cookie 的方法
- 在浏览器中打开 YouTube Music。
- 按 F12 → 切换到 Network(网络)标签页。
- 执行任意操作(例如播放一首歌曲)。
- 找到指向
music.youtube.com的请求,复制 Cookie 请求头的值。 - 将该字符串粘贴到你的脚本中(或存入环境变量)。
📂 完整 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);
});
注意: 上面的代码片段仅执行搜索和匹配步骤。
若要实际创建 YouTube Music 播放列表并添加曲目,请在匹配阶段之后调用createYTMusicPlaylist(参见前一节),并传入 cookie 字符串以及来自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 – 如果需要读取私人播放列表或向用户库写入,使用授权码流程。
- CLI 包装器 – 将脚本暴露为一个简单的命令行工具(
npx spotify-to-ytmusic …)。
🎉 编码愉快!
现在,你已经拥有一个完整的、基于 TypeScript 的流水线,只需几条命令即可将任意 Spotify 播放列表复制到 YouTube Music。随意修改代码、添加更多日志,或将其集成到更大的自动化套件中。
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);
📦 现成解决方案
- spotify-to-youtube – 用于匹配曲目的 NPM 包
- SpotTransfer – 完整的迁移工具(GUI)
- spotify_to_ytmusic – Python CLI 工具
🛠️ 实现稳健的技巧
- Rate limiting – 添加延迟(例如
300 ms)在 API 调用之间,以避免限流。 - Error handling – 某些曲目可能没有精确匹配;请记录这些以便手动审查。
- Fuzzy matching – 使用模糊字符串库(例如
fuzzball)来提升匹配准确性。 - Batch processing – 对于大型播放列表,分块处理并持久化中间结果。
- Logging – 保留失败匹配的 CSV/JSON,以便后续手动添加。
🚀 Extending the script
- Sync scheduling – 在 cron 任务中运行脚本以进行定期更新。
- Two‑way sync – 检测任一平台的添加/删除并保持同步。
- Web UI – 将逻辑封装在 Express.js 应用中,提供简易前端。
- Discord bot – 通过斜杠命令公开功能。
完整的源代码(包括上述脚本)已在 GitHub 上提供。祝编码愉快!
📚 资源
- Spotify Web API TypeScript SDK –
- ytmusic-api –
- youtube-music-ts-api –
- Spotify Developer Documentation –