Eric Way's Personal Site

I Write $\sin(x)$ Not Tragedies

从QQ音乐导入收藏歌单到Spotify(2022年)

2022-09-15 Coding

早年在 QQ 音乐收藏了600+首歌,后来因为各种原因转用Spotify。很想把歌单导出,但网路上现有的脚本基本都失效了,歌单转换平台 Soundiiz 也并不支持 QQ 音乐这个平台。

有人建议通过下载列表内的所有歌获取(可进一步处理的)歌曲列表,不过笔者现在人在海外,并不能下载,且很多当年 QQ 音乐有版权的歌现在也没版权了。 笔者首先尝试用Fiddler Everywhere抓取QQ音乐Linux 和 Windows 客户端的数据包,(可能)限于个人能力,并不成功(但在 Windows 客户端可以抓取到收听历史的json)。 现在是2022年9月,QQ音乐网页端的每个播放列表只能显示前10首歌。但可以注意到,QQ音乐网页端允许用户取消收藏任意歌曲,而且每次取消收藏后页面会显示新的前10首歌。因此思路是:每次获取当前页面的10首歌后,进行10次取消收藏操作,然后再获取当前页面的10首歌,如此重复。 但如此的话,程序运行结束,收藏的歌单就被清空了,怎么办?事实上QQ音乐提供了一键恢复功能(至少在Android端可用)。另外也可以提前把所有歌批量导出到一个新的歌单里进行备份。 代码并不困难,用 Tampermonkey 在 Firefox 运行脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// ==UserScript==
// @name QQ Music
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Fetch all your liked songs by deleting them.
// @author EricWay1024
// @match https://y.qq.com/n/ryqq/profile/like/song
// @icon https://www.google.com/s2/favicons?sz=64&domain=mozilla.org
// @grant none
// ==/UserScript==

function waitForElm(selector) {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}

const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector));
observer.disconnect();
}
});

observer.observe(document.body, {
childList: true,
subtree: true
});
});
}

function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}

(async function() {
'use strict';
await waitForElm('#like_song_box > div.mod_songlist > ul.songlist__list');
const all_names = [];
const all_artists = [];
let loop = true;
while (loop) {
[...document.getElementsByClassName('songlist__song_txt')].forEach(item => item.remove());
const names = [...document.getElementsByClassName('songlist__songname')]
.map(item => item.innerText.replace('\n播放', '').replace('\n添加到歌单\nVIP下载', ''));
const artists = [...document.getElementsByClassName('songlist__artist')]
.map(item => item.innerText.split(' /'));
all_names.push(...names);
all_artists.push(...artists);
console.log([all_names, all_artists]);

for (let i = 0; i < 10; i++) {
const buttons = document.getElementsByClassName('songlist__delete');
if (buttons.length === 0) {
console.log('Ending loop');
loop = false;
break;
}
buttons[0].click();
await delay(1000);
document.querySelector(
'body > div:nth-child(7) > div > div.yqq-dialog-wrap > div > div.yqq-dialog-content > div > div > div.popup__ft > button.upload_btns__item.mod_btn'
).click()
await delay(2000);
}
}
})();

安装此脚本后,登陆QQ音乐网页端并前往 https://y.qq.com/n/ryqq/profile/like/song 就可以了。输出在浏览器开发者工具的Console里面,右击输出然后copy object,复制到任何文本文件即可(这里命名为songs.json)。因为脚本写的很简单,所以有可能出现出错的情况。那样的话可能要多复制几次,最后手动合并一下(会导致一些歌重复)。 接下来是导入到Spotify了。这一步应该可以用Soundiiz实现,不过我还是利用Spotify API写了一个脚本,因为歌比较多(Soundiiz似乎有导入歌曲数量上限限制)。

  1. 手动在Spotify创建一个新歌单。
  2. 这里获取playlist id。先点击绿色的get token,然后给所需的scope打勾,即可获取token。接着点击try it,右侧就有请求结果,找到对应的播放列表的id这个key就可以了。
  3. 这里获取一个可以添加歌曲到播放列表的token,方法同上。
  4. 把token和playlist id替换到下面的代码里面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import json
import requests
from tqdm import tqdm

token = 'YOUR TOKEN'
playlist_id = 'YOUR PLAYLIST ID'

with open('songs.json') as f:
tracks, artists = json.load(f)
artists = [a[0] for a in artists] # Only keep the first artist

headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': f'Bearer {token}',
}

def get_search_items(q):
params = (
('q', q),
('type', 'track'),
('market', 'US'), # Or change to your market
)
response = requests.get('https://api.spotify.com/v1/search', headers=headers, params=params)
try:
return response.json()['tracks']['items']
except KeyError:
return []

for track, artist in tqdm(list(zip(tracks, artists))):
items = get_search_items(f'{track} artist:{artist}')
if len(items) == 0:
items = get_search_items(f'{track} {artist}')
if len(items) == 0:
print('Unable to find: ', track, artist)
continue
uri = items[0]['uri']
params = (
('uris', uri),
)
response = requests.post(f'https://api.spotify.com/v1/playlists/{playlist_id}/tracks', headers=headers, params=params)

有一些歌可能不能找到,在命令行会报错,尝试手动添加一下。也会有一些歌添加的版本可能不太对,手动删掉或替换掉(一般是因为找不到同样版本才会这样)。

This article was last updated on days ago, and the information described in the article may have changed.