pip install mutagen pillow –break-system-packages
python3 extract_album_art.py /media/MP3CARD/mp3/ /homeassistant/www/mp3/ -r
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MP3 앨범 아트 추출기 (커맨드라인 버전)
사용법:
python3 extract_album_art.py <원본폴더> <대상폴더> [옵션]
예시:
pip install mutagen pillow --break-system-packages
python3 extract_album_art.py ~/Music ~/Music/covers
python3 extract_album_art.py ~/Music ~/Music/covers -r
python3 extract_album_art.py ~/Music ~/Music/covers -r --overwrite
python3 extract_album_art.py /media/MP3CARD/mp3/ /homeassistant/www/mp3/ -r
"""
import os
import sys
import argparse
import io
from mutagen.id3 import ID3
from mutagen.mp3 import MP3
from mutagen import MutagenError
from PIL import Image
def find_album_art(mp3_path):
"""mp3 파일에서 앨범 아트(APIC 프레임) 바이너리 데이터를 반환. 없으면 None."""
try:
audio = MP3(mp3_path, ID3=ID3)
except MutagenError:
return None
if audio.tags is None:
return None
apic_frames = audio.tags.getall("APIC")
if not apic_frames:
return None
# 여러 개면 표지(front cover, type=3)를 우선 선택, 없으면 첫 번째
front = [f for f in apic_frames if getattr(f, "type", None) == 3]
frame = front[0] if front else apic_frames[0]
return frame.data
def save_as_jpg(image_bytes, out_path):
"""이미지 바이트를 실제 JPEG로 변환하여 저장 (원본이 PNG 등이어도 안전하게 jpg로 변환)."""
img = Image.open(io.BytesIO(image_bytes))
if img.mode in ("RGBA", "P", "LA"):
img = img.convert("RGB")
img.save(out_path, "JPEG", quality=95)
def extract_folder(src_dir, dst_dir, recursive=False, overwrite=False, quiet=False):
def log(msg):
if not quiet:
print(msg)
os.makedirs(dst_dir, exist_ok=True)
if recursive:
mp3_files = []
for root, _, files in os.walk(src_dir):
for f in files:
if f.lower().endswith(".mp3"):
mp3_files.append(os.path.join(root, f))
else:
mp3_files = [
os.path.join(src_dir, f)
for f in os.listdir(src_dir)
if f.lower().endswith(".mp3")
]
if not mp3_files:
log("MP3 파일을 찾을 수 없습니다.")
return 0, 0
success = 0
skipped = 0
for path in sorted(mp3_files):
base = os.path.splitext(os.path.basename(path))[0]
# 하위 폴더 구조를 대상 폴더에도 그대로 유지 (예: classic/song.mp3 -> classic/song.jpg)
rel_dir = os.path.relpath(os.path.dirname(path), src_dir)
out_dir = os.path.join(dst_dir, rel_dir) if rel_dir != "." else dst_dir
os.makedirs(out_dir, exist_ok=True)
out_path = os.path.join(out_dir, base + ".jpg")
if os.path.exists(out_path) and not overwrite:
log(f"[건너뜀] 이미 존재함: {os.path.basename(out_path)}")
skipped += 1
continue
try:
data = find_album_art(path)
if data is None:
log(f"[건너뜀] 앨범 이미지 없음: {os.path.basename(path)}")
skipped += 1
continue
save_as_jpg(data, out_path)
log(f"[완료] {os.path.basename(path)} -> {os.path.basename(out_path)}")
success += 1
except Exception as e:
log(f"[오류] {os.path.basename(path)}: {e}")
skipped += 1
return success, skipped
def main():
parser = argparse.ArgumentParser(
description="MP3 파일에서 앨범 아트를 추출하여 JPG로 저장합니다.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("source", help="MP3 파일이 있는 원본 폴더")
parser.add_argument("dest", help="JPG를 저장할 대상 폴더")
parser.add_argument(
"-r", "--recursive", action="store_true", help="하위 폴더까지 포함하여 검색"
)
parser.add_argument(
"--overwrite",
action="store_true",
help="대상 폴더에 이미 같은 이름의 jpg가 있어도 덮어쓰기 (기본: 건너뜀)",
)
parser.add_argument(
"-q", "--quiet", action="store_true", help="진행 로그를 출력하지 않고 결과만 출력"
)
args = parser.parse_args()
if not os.path.isdir(args.source):
print(f"오류: 원본 폴더가 존재하지 않습니다 -> {args.source}", file=sys.stderr)
sys.exit(1)
success, skipped = extract_folder(
args.source,
args.dest,
recursive=args.recursive,
overwrite=args.overwrite,
quiet=args.quiet,
)
print(f"\n총 {success}개 성공, {skipped}개 건너뜀/오류.")
if __name__ == "__main__":
main()