根据文件名写入音频文件标签

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()
posted @ 2026-09-05 00:23  wanghongwei-dev  阅读(21)  评论(0)    收藏  举报