import
sys
import
os
import
requests
import
urllib.parse
import
time
import
random
import
re
import
datetime
from
mutagen.id3
import
ID3, APIC
from
mutagen.mp3
import
MP3
if
getattr
(sys,
'frozen'
,
False
):
import
ctypes
ctypes.windll.kernel32.AttachConsole(
-
1
)
def
show_usage_info():
print
(
"="
*
80
)
print
(
"MP3专辑封面下载器 - 使用说明"
)
print
(
"="
*
80
)
print
(
"功能: 自动从QQ音乐、网易云音乐、酷狗音乐和酷我音乐下载MP3专辑封面并嵌入"
)
print
(
" 成功添加封面后可自动将文件重命名为 YYYYMMDD.原文件名.mp3 格式"
)
print
(
" 如文件已有封面,则只执行重命名操作(除非使用--no-rename选项)"
)
print
(
"\n可用命令行参数:"
)
print
(
" path - MP3文件或目录路径(默认为当前目录)"
)
print
(
" --delay 秒数 - 设置每首歌曲处理间隔时间(秒),默认0.5秒"
)
print
(
" --no-rename - 不重命名文件,仅添加封面"
)
print
(
" --force-download - 即使已有封面也强制重新下载"
)
print
(
" --verbose - 显示详细的处理信息"
)
print
(
"\n使用示例:"
)
print
(
" python mp3_cover_downloader.py D:\\音乐文件夹"
)
print
(
" python mp3_cover_downloader.py 单个文件.mp3 --delay 1"
)
print
(
" python mp3_cover_downloader.py --no-rename"
)
print
(
" python mp3_cover_downloader.py --force-download --verbose"
)
print
(
"="
*
80
)
print
()
def
get_cover_from_kugou(song_name, artist):
try
:
keyword
=
urllib.parse.quote(f
"{song_name} {artist}"
)
search_url
=
f
"https://songsearch.kugou.com/song_search_v2?keyword={keyword}&page=1&pagesize=1"
headers
=
{
'User-Agent'
:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
,
'Referer'
:
'https://www.kugou.com/'
}
response
=
requests.get(search_url, headers
=
headers, timeout
=
10
)
response.raise_for_status()
try
:
data
=
response.json()
except
ValueError:
print
(f
"酷狗音乐API返回了无效的JSON数据"
)
return
None
if
data.get(
'status'
)
=
=
1
and
data.get(
'data'
)
and
data[
'data'
].get(
'lists'
)
and
len
(data[
'data'
][
'lists'
]) >
0
:
song_info
=
data[
'data'
][
'lists'
][
0
]
file_hash
=
song_info.get(
'FileHash'
)
album_id
=
song_info.get(
'AlbumID'
)
if
file_hash:
song_url
=
f
"https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash={file_hash}&platid=4&mid=00000000000000000000000000000000"
song_headers
=
{
'User-Agent'
:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
,
'Referer'
: f
'https://www.kugou.com/song/#hash={file_hash}'
}
song_response
=
requests.get(song_url, headers
=
song_headers, timeout
=
10
)
try
:
song_data
=
song_response.json()
if
song_data.get(
'status'
)
=
=
1
and
song_data.get(
'data'
):
img_url
=
song_data[
'data'
].get(
'img'
)
if
img_url
and
img_url.startswith(
'http'
):
img_response
=
requests.get(img_url, timeout
=
10
)
if
img_response.status_code
=
=
200
:
return
img_response.content
except
(ValueError, KeyError) as e:
print
(f
"酷狗音乐歌曲API解析错误: {str(e)}"
)
except
Exception as e:
print
(f
"酷狗音乐API错误: {str(e)}"
)
return
None
def
get_cover_from_qq(song_name, artist):
try
:
keyword
=
urllib.parse.quote(f
"{song_name} {artist}"
)
search_url
=
f
"https://c.y.qq.com/soso/fcgi-bin/client_search_cp?w={keyword}&format=json&p=1&n=1"
headers
=
{
'User-Agent'
:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
,
'Referer'
:
'https://y.qq.com/'
,
'Accept'
:
'application/json'
}
response
=
requests.get(search_url, headers
=
headers, timeout
=
10
)
response.raise_for_status()
try
:
data
=
response.json()
except
ValueError:
print
(f
"QQ音乐API返回了无效的JSON数据"
)
return
None
if
data.get(
'code'
)
=
=
0
and
data.get(
'data'
)
and
data[
'data'
].get(
'song'
)
and
data[
'data'
][
'song'
].get(
'list'
)
and
len
(data[
'data'
][
'song'
][
'list'
]) >
0
:
song
=
data[
'data'
][
'song'
][
'list'
][
0
]
album_mid
=
song.get(
'albummid'
)
if
album_mid:
img_url
=
f
"https://y.gtimg.cn/music/photo_new/T002R800x800M000{album_mid}.jpg"
img_response
=
requests.get(img_url, headers
=
headers, timeout
=
10
)
if
img_response.status_code
=
=
200
and
len
(img_response.content) >
10000
:
return
img_response.content
img_url_small
=
f
"https://y.gtimg.cn/music/photo_new/T002R300x300M000{album_mid}.jpg"
img_response
=
requests.get(img_url_small, headers
=
headers, timeout
=
10
)
if
img_response.status_code
=
=
200
and
len
(img_response.content) >
5000
:
return
img_response.content
except
Exception as e:
print
(f
"QQ音乐API错误: {str(e)}"
)
return
None
def
get_cover_from_kuwo(song_name, artist):
try
:
csrf_token
=
'
'.join(random.choice('
ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
for
_
in
range
(
10
))
keyword
=
urllib.parse.quote(f
"{song_name} {artist}"
)
search_url
=
f
"http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key={keyword}&pn=1&rn=1"
headers
=
{
'User-Agent'
:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
,
'Referer'
: f
'http://www.kuwo.cn/search/list?key={keyword}'
,
'csrf'
: csrf_token,
'Cookie'
: f
'kw_token={csrf_token}'
}
session
=
requests.Session()
session.get(f
'http://www.kuwo.cn/search/list?key={keyword}'
, headers
=
{
'User-Agent'
:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
})
time.sleep(
1
)
response
=
session.get(search_url, headers
=
headers, timeout
=
10
)
response.raise_for_status()
try
:
data
=
response.json()
except
ValueError:
print
(f
"酷我音乐搜索API返回了无效的JSON数据"
)
return
None
if
data.get(
'code'
)
=
=
200
and
data.get(
'data'
)
and
data[
'data'
].get(
'list'
)
and
len
(data[
'data'
][
'list'
]) >
0
:
rid
=
data[
'data'
][
'list'
][
0
][
'rid'
]
album_url
=
f
"http://www.kuwo.cn/api/www/music/musicInfo?mid={rid}&httpsStatus=1"
album_response
=
session.get(album_url, headers
=
headers, timeout
=
10
)
try
:
album_data
=
album_response.json()
if
album_data.get(
'code'
)
=
=
200
and
album_data.get(
'data'
):
img_url
=
album_data[
'data'
].get(
'pic'
)
if
img_url
and
img_url.startswith(
'http'
):
img_response
=
requests.get(img_url, timeout
=
10
)
if
img_response.status_code
=
=
200
:
return
img_response.content
except
ValueError:
print
(f
"酷我音乐专辑API返回了无效的JSON数据"
)
except
Exception as e:
print
(f
"酷我音乐API错误: {str(e)}"
)
return
None
def
get_cover_from_netease(song_name, artist):
try
:
clean_song
=
re.sub(r
'\(.*?\)|\[.*?\]'
, '', song_name).strip()
clean_artist
=
re.sub(r
'\(.*?\)|\[.*?\]'
, '', artist).strip()
keyword
=
urllib.parse.quote(f
"{clean_song} {clean_artist}"
)
search_url
=
f
"https://music.163.com/api/search/pc?s={keyword}&type=1&limit=3"
headers
=
{
'User-Agent'
:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
,
'Referer'
:
'https://music.163.com/'
,
'Accept'
:
'application/json'
}
response
=
requests.get(search_url, headers
=
headers, timeout
=
10
)
response.raise_for_status()
try
:
data
=
response.json()
except
ValueError:
print
(f
"网易云音乐API返回了无效的JSON数据"
)
return
None
if
data.get(
'code'
)
=
=
200
and
data.get(
'result'
)
and
data[
'result'
].get(
'songs'
)
and
len
(data[
'result'
][
'songs'
]) >
0
:
best_match
=
None
for
song
in
data[
'result'
][
'songs'
][:
3
]:
song_name_api
=
song.get(
'name'
, '').lower()
artists
=
[artist.get(
'name'
, '
').lower() for artist in song.get('
artists', [])]
if
clean_song.lower()
in
song_name_api
or
song_name_api
in
clean_song.lower():
if
any
(clean_artist.lower()
in
a
or
a
in
clean_artist.lower()
for
a
in
artists):
best_match
=
song
break
if
not
best_match
and
data[
'result'
][
'songs'
]:
best_match
=
data[
'result'
][
'songs'
][
0
]
if
best_match:
album_id
=
best_match.get(
'album'
, {}).get(
'id'
)
pic_url
=
best_match.get(
'album'
, {}).get(
'picUrl'
)
if
pic_url
and
pic_url.startswith(
'http'
):
img_response
=
requests.get(pic_url, timeout
=
10
)
if
img_response.status_code
=
=
200
:
return
img_response.content
if
album_id:
album_url
=
f
"https://music.163.com/api/album/{album_id}"
album_response
=
requests.get(album_url, headers
=
headers, timeout
=
10
)
album_data
=
album_response.json()
if
album_data.get(
'code'
)
=
=
200
and
album_data.get(
'album'
):
pic_url
=
album_data[
'album'
].get(
'picUrl'
)
if
pic_url
and
pic_url.startswith(
'http'
):
img_response
=
requests.get(pic_url, timeout
=
10
)
if
img_response.status_code
=
=
200
:
return
img_response.content
except
Exception as e:
print
(f
"网易云音乐API错误: {str(e)}"
)
return
None
def
download_cover(song_name, artist):
print
(f
"正在为 {song_name} - {artist} 搜索封面..."
)
cover_data
=
get_cover_from_qq(song_name, artist)
if
cover_data:
print
(f
"从QQ音乐找到 {song_name} 的封面"
)
return
cover_data
cover_data
=
get_cover_from_netease(song_name, artist)
if
cover_data:
print
(f
"从网易云音乐找到 {song_name} 的封面"
)
return
cover_data
cover_data
=
get_cover_from_kugou(song_name, artist)
if
cover_data:
print
(f
"从酷狗音乐找到 {song_name} 的封面"
)
return
cover_data
cover_data
=
get_cover_from_kuwo(song_name, artist)
if
cover_data:
print
(f
"从酷我音乐找到 {song_name} 的封面"
)
return
cover_data
print
(f
"无法找到 {song_name} - {artist} 的专辑封面"
)
return
None
def
has_cover_art(audio):
try
:
for
key
in
audio.keys():
if
key.startswith(
'APIC'
):
if
len
(audio[key].data) >
1000
:
return
True
return
False
except
Exception as e:
print
(f
"检查封面时出错: {str(e)}"
)
return
False
def
rename_file_with_date_prefix(file_path):
try
:
current_date
=
datetime.datetime.now().strftime(
"%Y%m%d"
)
file_dir
=
os.path.dirname(file_path)
file_name
=
os.path.basename(file_path)
date_pattern
=
r
"^\d{8}\.|^\d{8}_\d+\."
if
re.match(date_pattern, file_name):
print
(f
"{file_name}: 文件名已包含日期前缀,无需重命名"
)
return
file_path
new_file_name
=
f
"{current_date}.{file_name}"
new_file_path
=
os.path.join(file_dir, new_file_name)
suffix
=
1
while
os.path.exists(new_file_path):
new_file_name
=
f
"{current_date}_{suffix}.{file_name}"
new_file_path
=
os.path.join(file_dir, new_file_name)
suffix
+
=
1
os.rename(file_path, new_file_path)
print
(f
"文件已重命名: {file_name} -> {new_file_name}"
)
return
new_file_path
except
Exception as e:
print
(f
"重命名文件失败: {str(e)}"
)
return
file_path
def
extract_info_from_filename(filename):
basename
=
filename.replace(
'.mp3'
, '')
date_match
=
re.match(r
'^\d{8}\.(.+)$|^\d{8}_\d+\.(.+)$'
, basename)
if
date_match:
basename
=
date_match.group(
1
)
if
date_match.group(
1
)
else
date_match.group(
2
)
parts
=
basename.split(
' - '
)
if
len
(parts) >
=
2
:
artist
=
parts[
0
].strip()
song_name
=
parts[
1
].strip()
else
:
artist
=
"Unknown"
song_name
=
basename
return
artist, song_name
def
process_mp3_file(file_path):
try
:
print
(f
"开始处理: {os.path.basename(file_path)}"
)
try
:
audio
=
ID3(file_path)
except
:
print
(f
"{os.path.basename(file_path)}: 未找到ID3标签,创建新标签"
)
audio
=
ID3()
audio.save(file_path)
audio
=
ID3(file_path)
mp3_audio
=
MP3(file_path, ID3
=
ID3)
song_name
=
""
artist
=
""
if
'TIT2'
in
audio:
song_name
=
str
(audio[
'TIT2'
])
print
(f
"获取到歌曲名: {song_name}"
)
if
'TPE1'
in
audio:
artist
=
str
(audio[
'TPE1'
])
print
(f
"获取到艺术家: {artist}"
)
if
not
song_name
or
not
artist:
filename
=
os.path.basename(file_path)
filename_artist, filename_song
=
extract_info_from_filename(filename)
if
not
artist:
artist
=
filename_artist
print
(f
"ID3标签中无艺术家,从文件名解析: {artist}"
)
if
not
song_name:
song_name
=
filename_song
print
(f
"ID3标签中无歌曲名,从文件名解析: {song_name}"
)
if
has_cover_art(audio):
print
(f
"{os.path.basename(file_path)}: 已有专辑封面,跳过下载"
)
new_path
=
rename_file_with_date_prefix(file_path)
return
True
print
(f
"开始下载专辑封面..."
)
cover_data
=
download_cover(song_name, artist)
if
not
cover_data:
print
(f
"{os.path.basename(file_path)}: 无法找到专辑封面(QQ音乐、网易云、酷狗和酷我音乐均无结果)"
)
return
False
print
(f
"写入专辑封面到文件..."
)
audio.add(APIC(
encoding
=
3
,
mime
=
'image/jpeg'
,
type
=
3
,
desc
=
u
'Cover'
,
data
=
cover_data
))
audio.save()
print
(f
"{os.path.basename(file_path)}: 成功添加专辑封面"
)
new_path
=
rename_file_with_date_prefix(file_path)
return
True
except
Exception as e:
print
(f
"{os.path.basename(file_path)}: 处理失败 - {str(e)}"
)
return
False
def
batch_process_directory(directory, delay
=
0.5
):
success_count
=
0
failure_count
=
0
mp3_files
=
[]
for
filename
in
os.listdir(directory):
if
filename.lower().endswith(
'.mp3'
):
mp3_files.append(os.path.join(directory, filename))
total_files
=
len
(mp3_files)
print
(f
"发现 {total_files} 个MP3文件需要处理"
)
for
index, file_path
in
enumerate
(mp3_files):
print
(f
"\n[{index+1}/{total_files}] 处理文件: {os.path.basename(file_path)}"
)
if
process_mp3_file(file_path):
success_count
+
=
1
else
:
failure_count
+
=
1
if
index < total_files
-
1
:
time.sleep(delay)
print
(f
"\n处理完成: 成功 {success_count} 个, 失败 {failure_count} 个"
)
if
__name__
=
=
"__main__"
:
import
argparse
print
(
"\nMP3专辑封面下载器 v1.0.0\n"
)
show_usage_info()
if
len
(sys.argv) >
1
and
os.path.exists(sys.argv[
1
])
and
'--'
not
in
sys.argv[
1
]:
drop_path
=
sys.argv[
1
]
sys.argv
=
[sys.argv[
0
]]
+
[
'--'
]
+
sys.argv[
1
:]
parser
=
argparse.ArgumentParser(description
=
'MP3专辑封面下载器'
)
parser.add_argument(
'path'
, nargs
=
'?'
, default
=
'.'
,
help
=
'MP3文件或目录路径(默认为当前目录)'
)
parser.add_argument(
'--delay'
,
type
=
float
, default
=
0.5
,
help
=
'每首歌曲处理间隔时间(秒),默认0.5秒'
)
parser.add_argument(
'--no-rename'
, action
=
'store_true'
,
help
=
'不重命名文件,仅添加封面'
)
parser.add_argument(
'--force-download'
, action
=
'store_true'
,
help
=
'即使已有封面也强制重新下载'
)
parser.add_argument(
'--verbose'
, action
=
'store_true'
,
help
=
'显示详细的处理信息'
)
args
=
parser.parse_args()
if
not
args.verbose:
original_print
=
print
def
filtered_print(
*
args,
*
*
kwargs):
if
args
and
isinstance
(args[
0
],
str
):
text
=
args[
0
]
if
any
(x
in
text
for
x
in
[
"获取到歌曲名"
,
"获取到艺术家"
,
"ID3标签中无"
,
"开始处理"
,
"开始下载"
,
"写入专辑封面"
]):
return
original_print(
*
args,
*
*
kwargs)
print
=
filtered_print
if
args.no_rename:
def
rename_file_with_date_prefix(file_path):
return
file_path
print
(
"已禁用文件重命名功能"
)
if
args.force_download:
def
has_cover_art(audio):
return
False
print
(
"已启用强制下载模式,将重新下载所有MP3的封面"
)
if
os.path.isfile(args.path):
process_mp3_file(args.path)
elif
os.path.isdir(args.path):
print
(f
"开始批量处理目录: {args.path}"
)
print
(f
"将搜索QQ音乐、网易云音乐、酷狗音乐和酷我音乐获取专辑封面"
)
print
(f
"处理间隔设置为 {args.delay} 秒"
)
batch_process_directory(args.path, args.delay)
else
:
print
(
"错误: 路径不存在"
)
if
getattr
(sys,
'frozen'
,
False
):
print
(
"\n处理完成,按任意键退出..."
)
try
:
input
()
except
:
pass