根据文件名写入音频文件标签
audio_tag_from_filename.py
#!/usr/bin/env python3
"""
根据文件名(格式:歌手 - 歌名)批量写入音频文件的 artist 和 title 标签。
支持 MP3 和 FLAC 格式,递归处理子目录。
支持 --dry-run 试运行模式,只显示将要修改的文件而不实际写入。
"""
import os
import sys
import argparse
from mutagen.easyid3 import EasyID3
from mutagen.flac import FLAC
# 支持的文件扩展名
AUDIO_EXTENSIONS = ('.mp3', '.flac')
def parse_filename(filepath):
"""从文件名解析艺术家和标题,格式:歌手 - 歌名.扩展名。
返回 (artist, title) 或 (None, None) 如果格式不匹配。
"""
filename = os.path.basename(filepath)
name_without_ext = os.path.splitext(filename)[0]
if ' - ' in name_without_ext:
artist, title = name_without_ext.split(' - ', 1)
artist = artist.strip()
title = title.strip()
if artist and title:
return artist, title
return None, None
def get_audio_tags(filepath):
"""读取音频文件的 artist 和 title 标签。
返回 (artist, title) 元组,缺失或读取失败返回 (None, None)。
"""
ext = os.path.splitext(filepath)[1].lower()
try:
if ext == '.mp3':
audio = EasyID3(filepath)
artist_list = audio.get('artist')
title_list = audio.get('title')
artist = artist_list[0] if artist_list else None
title = title_list[0] if title_list else None
return artist, title
elif ext == '.flac':
audio = FLAC(filepath)
artist_list = audio.get('artist')
title_list = audio.get('title')
artist = artist_list[0] if artist_list else None
title = title_list[0] if title_list else None
return artist, title
except Exception:
# 读取失败,返回 (None, None)
return None, None
return None, None
def set_audio_tags(filepath, artist, title):
"""写入音频文件的 artist 和 title 标签。
返回 True 表示写入成功,False 表示失败。
"""
ext = os.path.splitext(filepath)[1].lower()
try:
if ext == '.mp3':
audio = EasyID3(filepath)
audio['artist'] = [artist]
audio['title'] = [title]
audio.save()
return True
elif ext == '.flac':
audio = FLAC(filepath)
audio['artist'] = artist
audio['title'] = title
audio.save()
return True
except Exception as e:
print(f" 写入失败: {filepath}: {e}", file=sys.stderr)
return False
return False
def process_file(filepath, dry_run=False):
"""处理单个文件,返回状态:
- 'updated' : 标签已更新(或 dry-run 中将要更新)
- 'skipped' : 标签与文件名一致,跳过
- 'error' : 处理过程中出错(例如文件名格式不匹配或写入失败)
"""
artist_from_name, title_from_name = parse_filename(filepath)
if artist_from_name is None or title_from_name is None:
print(f"跳过(文件名格式不匹配): {os.path.basename(filepath)}")
return 'error'
current_artist, current_title = get_audio_tags(filepath)
# 如果现有标签与文件名完全一致,则跳过
if current_artist == artist_from_name and current_title == title_from_name:
return 'skipped'
# 需要更新标签
if dry_run:
print(f"[DRY-RUN] 将要更新: {os.path.basename(filepath)}")
print(f" 原标签: {current_artist} - {current_title}")
print(f" 新标签: {artist_from_name} - {title_from_name}")
return 'updated'
else:
success = set_audio_tags(filepath, artist_from_name, title_from_name)
if success:
print(f"已更新: {os.path.basename(filepath)}")
print(f" 原标签: {current_artist} - {current_title}")
print(f" 新标签: {artist_from_name} - {title_from_name}")
return 'updated'
else:
return 'error'
def main():
parser = argparse.ArgumentParser(
description='根据文件名批量设置音频文件的 artist 和 title 标签(支持 MP3 和 FLAC)。'
)
parser.add_argument(
'directory',
nargs='?',
default='.',
help='目标文件夹路径(默认为当前目录)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='试运行模式,只显示将要修改的文件,不实际写入标签'
)
args = parser.parse_args()
target_dir = args.directory
if not os.path.isdir(target_dir):
print(f"错误:目录不存在 - {target_dir}", file=sys.stderr)
sys.exit(1)
# 收集所有音频文件
audio_files = []
for root, dirs, files in os.walk(target_dir):
for file in files:
if file.lower().endswith(AUDIO_EXTENSIONS):
audio_files.append(os.path.join(root, file))
if not audio_files:
print("未找到 MP3 或 FLAC 文件。")
sys.exit(0)
print(f"共找到 {len(audio_files)} 个音频文件。")
if args.dry_run:
print("运行模式:试运行(--dry-run),不会实际修改文件。\n")
else:
print("运行模式:正式运行,将修改文件标签。\n")
updated_files = []
skipped_count = 0
error_count = 0
for filepath in audio_files:
status = process_file(filepath, dry_run=args.dry_run)
if status == 'updated':
updated_files.append(filepath)
elif status == 'skipped':
skipped_count += 1
else: # 'error'
error_count += 1
# 输出统计
print("\n" + "=" * 50)
print("处理完成!")
print(f"总文件数: {len(audio_files)}")
if args.dry_run:
print(f"将要更新: {len(updated_files)} 个")
else:
print(f"已更新: {len(updated_files)} 个")
print(f"跳过(标签匹配): {skipped_count} 个")
print(f"错误/跳过(格式不匹配或写入失败): {error_count} 个")
if updated_files:
print("\n" + ("将要处理" if args.dry_run else "已处理") + "的文件列表:")
for f in updated_files:
print(f)
if __name__ == "__main__":
main()
作者:wanghongwei
版权声明:本作品遵循<CC BY-NC-ND 4.0>版权协议,商业转载请联系作者获得授权,非商业转载请附上原文出处链接及本声明。

浙公网安备 33010602011771号