mirror of
https://github.com/Suxiaoqinx/Netease_url.git
synced 2025-09-14 11:36:45 +08:00
feat:简易版个人歌单批量下载
This commit is contained in:
parent
2d619125f0
commit
ef095c5b64
66
playlists/README.md
Normal file
66
playlists/README.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
## 介绍
|
||||||
|
|
||||||
|
该脚本能够批量下载歌单中的歌曲,并且将歌曲信息写入MP3文件的元数据中(仅支持MP3,标准音质和高音质)
|
||||||
|
|
||||||
|
目前使用演示站点的url,后期可以会进一步完善。现在需要可以自行改动
|
||||||
|
|
||||||
|
## 项目配置
|
||||||
|
项目根目录下运行该命令安装依赖
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 获取歌曲url
|
||||||
|
1. 浏览器打开[网易云官网](https://music.163.com/#/playlist)
|
||||||
|
2. 选择自己的歌单
|
||||||
|
3. 在对应的歌单列表的某一首歌上右击,点检查
|
||||||
|
4. 弹出开发者工具面板中切换到控制台页面(一般为第二个选项)
|
||||||
|
5. 将下面的脚本粘贴进控制台,回车
|
||||||
|
6. 复制整个输出内容到当前项目下的 `settings.json`文件(替换原来的内容)
|
||||||
|
7. 可以自行更改`settings.json`中的`level`和`savePath`两个参数,以调整歌曲下载的音质和保存位置
|
||||||
|
```js
|
||||||
|
const settings = {
|
||||||
|
"level": "exhigh",
|
||||||
|
"savePath": "./success",
|
||||||
|
"packPath": "./pack"
|
||||||
|
}
|
||||||
|
let spans;
|
||||||
|
let url = [];
|
||||||
|
|
||||||
|
spans = document.querySelectorAll(".ttc>.txt");
|
||||||
|
spans.forEach((span) => {
|
||||||
|
// 在每个 <span> 内选择 <a> 元素
|
||||||
|
const link = span.querySelector("a");
|
||||||
|
if (link) {
|
||||||
|
url.push(link.href)
|
||||||
|
} else {
|
||||||
|
console.log("No <a> tag found in this <span> tag.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
settings['songs'] = url
|
||||||
|
console.log(JSON.stringify(settings, null, 2));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下载歌曲
|
||||||
|
> 提示:前提条件,完成项目配置,以及settings.json的内容
|
||||||
|
>
|
||||||
|
|
||||||
|
执行parse.py
|
||||||
|
```bash
|
||||||
|
python parse.py
|
||||||
|
```
|
||||||
|
|
||||||
|
等待进度条走完后,检查新生成的`errorList.json`其中记录着失败的歌曲,`completedList.json`记录着全部成功的歌曲。
|
||||||
|
|
||||||
|
自行检查`errorList.json`、`completedList.json`中的数据,当前success下已经有了对应的歌曲文件
|
||||||
|
|
||||||
|
## 歌曲信息打包
|
||||||
|
> 该步骤将success中下载的歌曲数据(封面-作者)等相关数据打包到MP3文件中
|
||||||
|
>
|
||||||
|
> 如不需要相关信息可以跳过此步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python pack.py
|
||||||
|
```
|
66
playlists/pack.py
Normal file
66
playlists/pack.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import requests
|
||||||
|
from mutagen.easyid3 import EasyID3
|
||||||
|
from mutagen.id3 import ID3, APIC, USLT
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
def update_mp3_metadata(mp3_path, data, folder_path):
|
||||||
|
audio = EasyID3(mp3_path)
|
||||||
|
audio['title'] = data['name']
|
||||||
|
audio['artist'] = data['artist']
|
||||||
|
audio['album'] = data['album']
|
||||||
|
audio['albumartist'] = data['artist']
|
||||||
|
audio.save()
|
||||||
|
|
||||||
|
audio = ID3(mp3_path)
|
||||||
|
audio.update_to_v23() # 把可能存在的旧版本升级为2.3版本
|
||||||
|
# 添加封面
|
||||||
|
cover_path = os.path.join(folder_path, 'cover.jpg')
|
||||||
|
if os.path.exists(cover_path):
|
||||||
|
with open(cover_path, 'rb') as f:
|
||||||
|
cover_data = f.read()
|
||||||
|
audio['APIC'] = APIC(
|
||||||
|
encoding=0, # 3 is for utf-8
|
||||||
|
mime='image/jpeg', # image/jpeg or image/png
|
||||||
|
type=3, # 3 is for the cover(front) image
|
||||||
|
desc='Cover',
|
||||||
|
data=cover_data
|
||||||
|
)
|
||||||
|
# 添加歌词
|
||||||
|
if 'lrc' in data:
|
||||||
|
audio['USLT'] = USLT(
|
||||||
|
encoding=3,
|
||||||
|
lang='eng',
|
||||||
|
desc='Lyrics',
|
||||||
|
text=data['lrc']
|
||||||
|
)
|
||||||
|
audio.save()
|
||||||
|
|
||||||
|
def process_folder(folder_path, new_folder_path):
|
||||||
|
data_path = os.path.join(folder_path, 'data.json')
|
||||||
|
mp3_path = os.path.join(folder_path, 'song.mp3')
|
||||||
|
|
||||||
|
if os.path.exists(data_path) and os.path.exists(mp3_path):
|
||||||
|
with open(data_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
new_mp3_path = os.path.join(new_folder_path, f"{data['name']}-{data['artist']}.mp3")
|
||||||
|
shutil.copy(mp3_path, new_mp3_path)
|
||||||
|
update_mp3_metadata(new_mp3_path, data, folder_path)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with open("settings.json", "r", encoding="utf-8") as f:
|
||||||
|
settings = json.load(f)
|
||||||
|
success_folder = settings.get("./success", "./success")
|
||||||
|
pack_folder = settings.get("packPath", "./pack")
|
||||||
|
if not os.path.exists(pack_folder):
|
||||||
|
os.makedirs(pack_folder)
|
||||||
|
|
||||||
|
dirs = [d for d in os.listdir(success_folder) if os.path.isdir(os.path.join(success_folder, d))]
|
||||||
|
for dir_name in tqdm(dirs, desc="Processing folders"):
|
||||||
|
folder_path = os.path.join(success_folder, dir_name)
|
||||||
|
process_folder(folder_path, pack_folder)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
123
playlists/parse.py
Normal file
123
playlists/parse.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import requests
|
||||||
|
from pprint import pprint
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"accept": "application/json, text/plain, */*",
|
||||||
|
"accept-language": "zh-CN,zh;q=0.9",
|
||||||
|
"authorization": "Bearer def09b44baa4ea815d604f03a44993ce",
|
||||||
|
"content-type": "application/json",
|
||||||
|
"priority": "u=1, i",
|
||||||
|
"sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"",
|
||||||
|
"sec-ch-ua-full-version-list": "\"Not(A:Brand\";v=\"99.0.0.0\", \"Google Chrome\";v=\"133.0.6943.127\", \"Chromium\";v=\"133.0.6943.127\"",
|
||||||
|
"sec-ch-ua-mobile": "?0",
|
||||||
|
"sec-ch-ua-model": "\"\"",
|
||||||
|
"sec-ch-ua-platform": "\"Windows\"",
|
||||||
|
"sec-ch-ua-platform-version": "\"15.0.0\"",
|
||||||
|
"sec-fetch-dest": "empty",
|
||||||
|
"sec-fetch-mode": "cors",
|
||||||
|
"sec-fetch-site": "same-origin",
|
||||||
|
"Referer": "https://api.toubiec.cn/wyapi.html",
|
||||||
|
"Referrer-Policy": "strict-origin-when-cross-origin"
|
||||||
|
}
|
||||||
|
|
||||||
|
url = "https://api.toubiec.cn/api/music_v1.php"
|
||||||
|
|
||||||
|
|
||||||
|
with open("settings.json", "r", encoding="utf-8") as f:
|
||||||
|
settings = json.load(f)
|
||||||
|
songs_to_download = settings["songs"]
|
||||||
|
level = settings.get("level", "lossless")
|
||||||
|
save_path = settings["savePath"]
|
||||||
|
|
||||||
|
def download_file(file_url, output_location_path):
|
||||||
|
response = requests.get(file_url, stream=True)
|
||||||
|
with open(output_location_path, "wb") as f:
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
completed_list = []
|
||||||
|
error_list = []
|
||||||
|
|
||||||
|
# 创建日志文件夹
|
||||||
|
log_folder = "./log"
|
||||||
|
if not os.path.exists(log_folder):
|
||||||
|
os.makedirs(log_folder)
|
||||||
|
|
||||||
|
for i, song_url in enumerate(tqdm(songs_to_download, desc="Downloading songs Task", unit="song")):
|
||||||
|
data = {
|
||||||
|
"url": song_url,
|
||||||
|
"level": level,
|
||||||
|
"type": "song",
|
||||||
|
"token":"e7519f587b3cf92583d7388639eae7ee"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = requests.post(url, json=data, headers=headers)
|
||||||
|
|
||||||
|
if result.status_code != 200:
|
||||||
|
raise Exception(f"Status code: {result.status_code}")
|
||||||
|
|
||||||
|
# 解析结果
|
||||||
|
resultData = result.json()
|
||||||
|
|
||||||
|
# 校验
|
||||||
|
if resultData['status'] != 200:
|
||||||
|
raise Exception(f"Status code: {resultData['status']}")
|
||||||
|
|
||||||
|
# pprint(resultData)
|
||||||
|
|
||||||
|
cover = resultData['song_info']['cover']
|
||||||
|
name = resultData['song_info']['name']
|
||||||
|
artist = resultData['song_info']['artist']
|
||||||
|
alia = resultData['song_info']['alia']
|
||||||
|
album = resultData['song_info']['album']
|
||||||
|
song_original_url = resultData['url_info']['url']
|
||||||
|
song_type = resultData['url_info']['type']
|
||||||
|
lrc = resultData['lrc']['lyric']
|
||||||
|
|
||||||
|
song_data = {
|
||||||
|
"cover": cover,
|
||||||
|
"name": name,
|
||||||
|
"artist": artist,
|
||||||
|
"alia": alia,
|
||||||
|
"album": album,
|
||||||
|
"song_original_url": song_original_url,
|
||||||
|
"song_type": song_type,
|
||||||
|
"lrc": lrc
|
||||||
|
}
|
||||||
|
|
||||||
|
output_folder_path = os.path.join(save_path, f"{name}-{artist}")
|
||||||
|
if not os.path.exists(output_folder_path):
|
||||||
|
os.makedirs(output_folder_path)
|
||||||
|
|
||||||
|
mp3_path = os.path.join(output_folder_path, f"song.{song_type}")
|
||||||
|
download_file(song_original_url, mp3_path)
|
||||||
|
|
||||||
|
cover_path = os.path.join(output_folder_path, "cover.jpg")
|
||||||
|
download_file(cover, cover_path)
|
||||||
|
|
||||||
|
data_path = os.path.join(output_folder_path, "data.json")
|
||||||
|
with open(data_path, "w", encoding="utf-8") as data_file:
|
||||||
|
json.dump(song_data, data_file, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
completed_list.append(song_url)
|
||||||
|
with open("./log/completedList.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(completed_list, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# 随机等待 1 到 3 秒
|
||||||
|
time.sleep(random.randint(1, 3))
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as error:
|
||||||
|
error_list.append({"url": song_url, "error": str(error)})
|
||||||
|
with open("./log/errorList.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(error_list, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
3
playlists/requirements.txt
Normal file
3
playlists/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
requests
|
||||||
|
tqdm
|
||||||
|
mutagen
|
8
playlists/settings.json
Normal file
8
playlists/settings.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"level": "exhigh",
|
||||||
|
"savePath": "./success",
|
||||||
|
"packPath": "./pack",
|
||||||
|
"songs": [
|
||||||
|
"https://music.163.com/song?id=2647373433"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user