feat: 新增批量下载歌曲工具
This commit is contained in:
parent
4951cea269
commit
4330f61888
1
.gitignore
vendored
1
.gitignore
vendored
@ -168,3 +168,4 @@ conf
|
|||||||
setting.json
|
setting.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
cache
|
cache
|
||||||
|
tmp/
|
||||||
|
8
test/test_remove_common_prefix.py
Normal file
8
test/test_remove_common_prefix.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from xiaomusic.utils import (
|
||||||
|
remove_common_prefix,
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
remove_common_prefix(
|
||||||
|
"./tmp/【无损音质】2024年9月酷狗热歌榜TOP100合集(只选热歌最高的)首首王炸,分P合集!"
|
||||||
|
)
|
@ -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)
|
||||||
|
@ -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>
|
||||||
|
113
xiaomusic/static/default/downloadtool.html
Normal file
113
xiaomusic/static/default/downloadtool.html
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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}"')
|
||||||
|
Loading…
Reference in New Issue
Block a user