MP3 앨범 이미지 추출 프로그램

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()