feat: 新增批量下载歌曲工具

This commit is contained in:
涵曦 2024-09-27 19:03:49 +08:00
parent 4951cea269
commit 4330f61888
9 changed files with 288 additions and 10 deletions

1
.gitignore vendored
View File

@ -168,3 +168,4 @@ conf
setting.json setting.json
.DS_Store .DS_Store
cache cache
tmp/

View File

@ -0,0 +1,8 @@
from xiaomusic.utils import (
remove_common_prefix,
)
if __name__ == "__main__":
remove_common_prefix(
"./tmp/【无损音质】2024年9月酷狗热歌榜TOP100合集只选热歌最高的首首王炸分P合集"
)

View File

@ -25,8 +25,11 @@ from starlette.responses import FileResponse, Response
from xiaomusic import __version__ from xiaomusic import __version__
from xiaomusic.utils import ( from xiaomusic.utils import (
deepcopy_data_no_sensitive_info, deepcopy_data_no_sensitive_info,
download_one_music,
download_playlist,
downloadfile, downloadfile,
get_latest_version, get_latest_version,
remove_common_prefix,
) )
xiaomusic = None xiaomusic = None
@ -369,6 +372,58 @@ async def latest_version(Verifcation=Depends(verification)):
return {"ret": "Fetch version failed"} return {"ret": "Fetch version failed"}
class DownloadPlayList(BaseModel):
dirname: str
url: str
# 下载歌单
@app.post("/downloadplaylist")
async def downloadplaylist(data: DownloadPlayList, Verifcation=Depends(verification)):
try:
download_proc = await download_playlist(config, data.url, data.dirname)
async def check_download_proc():
# 等待子进程完成
exit_code = await download_proc.wait()
log.info(f"Download completed with exit code {exit_code}")
dir_path = os.path.join(config.download_path, data.dirname)
log.debug(f"Download dir_path: {exit_code}")
if exit_code == 0:
log.info("Download was successful.")
# 执行成功的后续逻辑:文件名处理
remove_common_prefix(dir_path)
else:
# 处理失败的情况
log.error("Download failed.")
asyncio.create_task(check_download_proc())
return {"ret": "OK"}
except Exception as e:
log.exception(f"Execption {e}")
return {"ret": "Failed download"}
class DownloadOneMusic(BaseModel):
name: str = ""
url: str
# 下载单首歌曲
@app.post("/downloadonemusic")
async def downloadonemusic(data: DownloadOneMusic, Verifcation=Depends(verification)):
try:
await download_one_music(config, data.url, data.name)
return {"ret": "OK"}
except Exception as e:
log.exception(f"Execption {e}")
return {"ret": "Failed download"}
async def file_iterator(file_path, start, end): async def file_iterator(file_path, start, end):
async with aiofiles.open(file_path, mode="rb") as file: async with aiofiles.open(file_path, mode="rb") as file:
await file.seek(start) await file.seek(start)

View File

@ -6,9 +6,9 @@
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<title>Debug For XiaoMusic</title> <title>Debug For XiaoMusic</title>
<link rel="stylesheet" type="text/css" href="./style.css?version=1727033309"> <link rel="stylesheet" type="text/css" href="./style.css?version=1727434231">
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script> <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script src="./jquery-3.7.1.min.js?version=1727033309"></script> <script src="./jquery-3.7.1.min.js?version=1727434231"></script>
<!-- Google tag (gtag.js) --> <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>

View File

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>歌曲下载工具</title>
<link rel="stylesheet" type="text/css" href="./style.css?version=1727434231">
<script src="./jquery-3.7.1.min.js?version=1727434231"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
</head>
<body>
<h1>歌曲下载工具</h1>
<div class="rows">
<!-- 歌单的输入 -->
<label for="playlistUrl">输入歌单 URL:</label>
<input type="text" id="playlistUrl" value="https://m.bilibili.com/video/BV1WUsDezE88">
<label for="dirname">输入歌单名字:</label>
<input type="text" id="dirname" placeholder="流行歌曲">
<button id="downloadPlaylistBtn">下载歌单</button>
</div>
<hr>
<div class="rows">
<!-- 单曲的输入 -->
<label for="songUrl">输入歌曲 URL:</label>
<input type="text" id="songUrl" value="https://m.bilibili.com/video/BV1qD4y1U7fs">
<label for="songName">输入歌曲名字:</label>
<input type="text" id="songName" placeholder="歌曲名">
<button id="downloadSongBtn">下载单曲</button>
</div>
<script>
// 下载歌单
$('#downloadPlaylistBtn').click(function() {
var playlistUrl = $('#playlistUrl').val();
var dirname = $('#dirname').val();
if (!playlistUrl || !dirname) {
alert('请填写完整的歌单 URL 和歌单名字');
return;
}
var data = {
dirname: dirname,
url: playlistUrl
};
$.ajax({
type: "POST",
url: "/downloadplaylist",
contentType: "application/json",
data: JSON.stringify(data),
success: (msg) => {
alert('歌单下载请求已发送!');
console.log(response);
},
error: (msg) => {
alert('歌单下载请求失败,请重试。');
}
});
});
// 下载单曲
$('#downloadSongBtn').click(function() {
var songName = $('#songName').val();
var songUrl = $('#songUrl').val();
if (!songUrl || !songName) {
alert('请填写完整的歌曲 URL 和歌曲名字');
return;
}
var data = {
name: songName,
url: songUrl
};
$.ajax({
type: "POST",
url: "/downloadonemusic",
contentType: "application/json",
data: JSON.stringify(data),
success: (msg) => {
alert('单曲下载请求已发送!');
console.log(response);
},
error: (msg) => {
alert('单曲下载请求失败,请重试。');
}
});
});
</script>
</body>
</html>

View File

@ -4,9 +4,9 @@
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title> <title>小爱音箱操控面板</title>
<script src="./jquery-3.7.1.min.js?version=1727033309"></script> <script src="./jquery-3.7.1.min.js?version=1727434231"></script>
<script src="./app.js?version=1727033309"></script> <script src="./app.js?version=1727434231"></script>
<link rel="stylesheet" type="text/css" href="./style.css?version=1727033309"> <link rel="stylesheet" type="text/css" href="./style.css?version=1727434231">
<!-- Google tag (gtag.js) --> <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>

View File

@ -5,7 +5,7 @@
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<title>M3U to JSON Converter</title> <title>M3U to JSON Converter</title>
<link rel="stylesheet" type="text/css" href="./style.css?version=1727033309"> <link rel="stylesheet" type="text/css" href="./style.css?version=1727434231">
<!-- Google tag (gtag.js) --> <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>

View File

@ -4,9 +4,9 @@
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title> <title>小爱音箱操控面板</title>
<script src="./jquery-3.7.1.min.js?version=1727033309"></script> <script src="./jquery-3.7.1.min.js?version=1727434231"></script>
<script src="./setting.js?version=1727033309"></script> <script src="./setting.js?version=1727434231"></script>
<link rel="stylesheet" type="text/css" href="./style.css?version=1727033309"> <link rel="stylesheet" type="text/css" href="./style.css?version=1727434231">
<!-- Google tag (gtag.js) --> <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
@ -195,11 +195,12 @@ var vConsole = new window.VConsole();
<button id="get_music_list">获取歌单</button> <button id="get_music_list">获取歌单</button>
<button id="refresh_music_tag">刷新tag</button> <button id="refresh_music_tag">刷新tag</button>
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<hr> <hr>
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<button onclick="location.href='/docs';">查看接口文档</button> <button onclick="location.href='/docs';">查看接口文档</button>
<a class="button" href="./m3u.html" target="_blank">m3u文件转换</a> <a class="button" href="./m3u.html" target="_blank">m3u文件转换</a>
<a class="button" href="./downloadtool.html" target="_blank">歌曲下载工具</a>
<hr> <hr>
<a class="button" href="./debug.html" target="_blank">调试工具</a> <a class="button" href="./debug.html" target="_blank">调试工具</a>

View File

@ -674,3 +674,103 @@ def extract_audio_metadata(file_path, save_root):
metadata.artist = _get_tag_value(tags, "Artist") metadata.artist = _get_tag_value(tags, "Artist")
return asdict(metadata) return asdict(metadata)
# 下载播放列表
async def download_playlist(config, url, dirname):
title = f"{dirname}/%(title)s.%(ext)s"
sbp_args = (
"yt-dlp",
"--yes-playlist",
"-x",
"--audio-format",
"mp3",
"--paths",
config.download_path,
"-o",
title,
"--ffmpeg-location",
f"{config.ffmpeg_location}",
)
if config.proxy:
sbp_args += ("--proxy", f"{config.proxy}")
sbp_args += (url,)
cmd = " ".join(sbp_args)
logging.info(f"download_playlist: {cmd}")
download_proc = await asyncio.create_subprocess_exec(*sbp_args)
return download_proc
# 下载一首歌曲
async def download_one_music(config, url, name=""):
title = "%(title)s.%(ext)s"
if name:
title = f"{name}.%(ext)s"
sbp_args = (
"yt-dlp",
"--no-playlist",
"-x",
"--audio-format",
"mp3",
"--paths",
config.download_path,
"-o",
title,
"--ffmpeg-location",
f"{config.ffmpeg_location}",
)
if config.proxy:
sbp_args += ("--proxy", f"{config.proxy}")
sbp_args += (url,)
cmd = " ".join(sbp_args)
logging.info(f"download_one_music: {cmd}")
download_proc = await asyncio.create_subprocess_exec(*sbp_args)
return download_proc
def _longest_common_prefix(file_names):
if not file_names:
return ""
# 将第一个文件名作为初始前缀
prefix = file_names[0]
for file_name in file_names[1:]:
while not file_name.startswith(prefix):
# 如果当前文件名不以prefix开头则缩短prefix
prefix = prefix[:-1]
if not prefix:
return ""
return prefix
# 移除目录下文件名前缀相同的
def remove_common_prefix(directory):
files = os.listdir(directory)
# 获取所有文件的前缀
common_prefix = _longest_common_prefix(files)
logging.info(f'Common prefix identified: "{common_prefix}"')
for filename in files:
if filename == common_prefix:
continue
# 检查文件名是否以共同前缀开头
if filename.startswith(common_prefix):
# 构造新的文件名
new_filename = filename[len(common_prefix) :]
# 生成完整的文件路径
old_file_path = os.path.join(directory, filename)
new_file_path = os.path.join(directory, new_filename)
# 重命名文件
os.rename(old_file_path, new_file_path)
logging.debug(f'Renamed: "{filename}" to "{new_filename}"')