#!/usr/bin/env python3

import base64
import os
import struct
import subprocess
import sys
from pathlib import Path


# ============================================================
# BitChord WebmTagger.kt - EBML IDs
# ============================================================

ID_EBML = bytes.fromhex("1A45DFA3")
ID_SEGMENT = bytes.fromhex("18538067")

ID_TAGS = bytes.fromhex("1254C367")
ID_TAG = bytes.fromhex("7373")
ID_TARGETS = bytes.fromhex("63C0")
ID_SIMPLETAG = bytes.fromhex("67C8")
ID_TAGNAME = bytes.fromhex("45A3")
ID_TAGSTRING = bytes.fromhex("4487")

ID_ATTACHMENTS = bytes.fromhex("1941A469")
ID_ATTACHEDFILE = bytes.fromhex("61A7")
ID_FILENAME = bytes.fromhex("466E")
ID_FILEDESCRIPTION = bytes.fromhex("467E")
ID_FILEMIMETYPE = bytes.fromhex("4660")
ID_FILEDATA = bytes.fromhex("465C")
ID_FILEUID = bytes.fromhex("46AE")


# ============================================================
# EBML VINT
# ============================================================

def read_vint(data, offset):
    if offset >= len(data):
        raise ValueError("truncated VINT")

    first = data[offset]

    mask = 0x80
    width = 1

    while width <= 8 and not (first & mask):
        mask >>= 1
        width += 1

    if width > 8 or offset + width > len(data):
        raise ValueError("invalid VINT")

    value = first & (mask - 1)

    for i in range(1, width):
        value = (value << 8) | data[offset + i]

    unknown = value == (1 << (7 * width)) - 1

    return value, width, unknown


def read_element_id(data, offset):
    if offset >= len(data):
        raise ValueError("truncated element ID")

    first = data[offset]

    mask = 0x80
    width = 1

    while width <= 4 and not (first & mask):
        mask >>= 1
        width += 1

    if width > 4:
        raise ValueError("invalid element ID")

    if offset + width > len(data):
        raise ValueError("truncated element ID")

    return data[offset:offset + width], width


def iter_elements(data, start=0, end=None):
    if end is None:
        end = len(data)

    offset = start

    while offset < end:

        element_id, id_width = read_element_id(
            data,
            offset
        )

        size, size_width, unknown = read_vint(
            data,
            offset + id_width
        )

        content_start = (
            offset
            + id_width
            + size_width
        )

        if unknown:
            raise ValueError(
                f"unknown-sized element: {element_id.hex()}"
            )

        content_end = content_start + size

        if content_end > end:
            raise ValueError(
                f"element exceeds parent: {element_id.hex()}"
            )

        yield (
            element_id,
            data[content_start:content_end],
            offset,
            content_end,
        )

        offset = content_end


# ============================================================
# WebM Segment
# ============================================================

def find_segment(data):

    element_id, id_width = read_element_id(
        data,
        0
    )

    if element_id != ID_EBML:
        raise ValueError("Not an EBML file")

    header_size, header_width, unknown = read_vint(
        data,
        id_width
    )

    if unknown:
        raise ValueError(
            "EBML header has unknown size"
        )

    segment_offset = (
        id_width
        + header_width
        + header_size
    )

    segment_id, segment_id_width = read_element_id(
        data,
        segment_offset
    )

    if segment_id != ID_SEGMENT:
        raise ValueError(
            "Segment element not found"
        )

    segment_size, segment_size_width, unknown = read_vint(
        data,
        segment_offset + segment_id_width
    )

    segment_content_start = (
        segment_offset
        + segment_id_width
        + segment_size_width
    )

    if unknown:
        return segment_content_start, len(data)

    segment_content_end = (
        segment_content_start
        + segment_size
    )

    if segment_content_end > len(data):
        raise ValueError(
            "Segment exceeds file"
        )

    return (
        segment_content_start,
        segment_content_end
    )


# ============================================================
# WebM Tags
# ============================================================

def decode_utf8(data):
    return data.decode(
        "utf-8",
        errors="replace"
    )


def parse_simple_tag(payload, result):

    name = None
    value = None

    for element_id, content, _, _ in iter_elements(payload):

        if element_id == ID_TAGNAME:

            name = decode_utf8(content)

        elif element_id == ID_TAGSTRING:

            value = decode_utf8(content)

        elif element_id == ID_SIMPLETAG:

            parse_simple_tag(
                content,
                result
            )

    if name is not None and value is not None:

        result.append(
            (name, value)
        )


def parse_tags(payload):

    result = []

    for element_id, content, _, _ in iter_elements(payload):

        if element_id == ID_SIMPLETAG:

            parse_simple_tag(
                content,
                result
            )

        elif element_id == ID_TAG:

            for child_id, child_content, _, _ in iter_elements(content):

                if child_id == ID_SIMPLETAG:

                    parse_simple_tag(
                        child_content,
                        result
                    )

    return result


# ============================================================
# WebM Attachments
# ============================================================

def parse_attached_file(payload):

    result = {}

    for element_id, content, _, _ in iter_elements(payload):

        if element_id == ID_FILENAME:

            result["filename"] = decode_utf8(content)

        elif element_id == ID_FILEDESCRIPTION:

            result["description"] = decode_utf8(content)

        elif element_id == ID_FILEMIMETYPE:

            result["mime"] = content.decode(
                "ascii",
                errors="replace"
            )

        elif element_id == ID_FILEDATA:

            result["data"] = content

    return result


def parse_attachments(payload):

    result = []

    for element_id, content, _, _ in iter_elements(payload):

        if element_id == ID_ATTACHEDFILE:

            result.append(
                parse_attached_file(content)
            )

    return result


# ============================================================
# WebM metadata 추출
# ============================================================

def extract_metadata(webm_path):

    data = Path(webm_path).read_bytes()

    segment_start, segment_end = find_segment(
        data
    )

    tags = []
    attachments = []

    for element_id, content, _, _ in iter_elements(
        data,
        segment_start,
        segment_end
    ):

        if element_id == ID_TAGS:

            tags.extend(
                parse_tags(content)
            )

        elif element_id == ID_ATTACHMENTS:

            attachments.extend(
                parse_attachments(content)
            )

    return tags, attachments


# ============================================================
# Cover 찾기
# ============================================================

def find_cover(attachments):

    for attachment in attachments:

        data = attachment.get("data")

        if not data:
            continue

        mime = (
            attachment.get("mime")
            or ""
        ).lower()

        filename = (
            attachment.get("filename")
            or ""
        ).lower()

        if mime.startswith("image/"):
            return attachment

        if filename.endswith(
            (
                ".jpg",
                ".jpeg",
                ".png",
                ".webp",
            )
        ):
            return attachment

    return None


# ============================================================
# 이미지 크기 / bit depth
# ============================================================

def get_image_info(data, mime):

    # PNG
    if data.startswith(b"\\x89PNG\\r\\n\\x1a\\n"):
        if len(data) >= 29:
            width = int.from_bytes(data[16:20], "big")
            height = int.from_bytes(data[20:24], "big")
            bit_depth = data[24]

            color_type = data[25]

            channels = {
                0: 1,
                2: 3,
                3: 1,
                4: 2,
                6: 4,
            }.get(color_type, 3)

            return width, height, bit_depth * channels, 0

    # JPEG
    if data.startswith(b"\\xff\\xd8"):
        pos = 2

        while pos + 9 < len(data):

            if data[pos] != 0xFF:
                pos += 1
                continue

            marker = data[pos + 1]

            if marker in (
                0xD8,
                0xD9,
            ):
                pos += 2
                continue

            if pos + 4 > len(data):
                break

            length = int.from_bytes(
                data[pos + 2:pos + 4],
                "big"
            )

            if length < 2:
                break

            # SOF markers
            if marker in (
                0xC0, 0xC1, 0xC2,
                0xC3, 0xC5, 0xC6,
                0xC7, 0xC9, 0xCA,
                0xCB, 0xCD, 0xCE,
                0xCF,
            ):
                if pos + 9 <= len(data):

                    bits = data[pos + 4]
                    height = int.from_bytes(
                        data[pos + 5:pos + 7],
                        "big"
                    )
                    width = int.from_bytes(
                        data[pos + 7:pos + 9],
                        "big"
                    )
                    channels = data[pos + 9]

                    return (
                        width,
                        height,
                        bits * channels,
                        0
                    )

            pos += 2 + length

    # WebP VP8X
    if data.startswith(b"RIFF") and data[8:12] == b"WEBP":

        if data[12:16] == b"VP8X" and len(data) >= 30:

            width = (
                1
                + int.from_bytes(
                    data[24:27],
                    "little"
                )
            )

            height = (
                1
                + int.from_bytes(
                    data[27:30],
                    "little"
                )
            )

            # VP8X supports RGB/RGBA
            depth = 32 if (data[20] & 0x10) else 24

            return width, height, depth, 0

    return 0, 0, 0, 0


# ============================================================
# FLAC Picture Block
#
# Ogg Opus의 METADATA_BLOCK_PICTURE에 사용
# ============================================================

def build_picture_block(cover):


    data = cover.get("data")

    if not data:
        return None

    mime = (
        cover.get("mime")
        or "image/jpeg"
    )

    description = (
        cover.get("description")
        or "Cover (front)"
    )

    description_bytes = (
        description.encode("utf-8")
    )

    # FLAC PICTURE block
    #
    # type             4
    # mime length      4
    # mime             N
    # description len  4
    # description      N
    # width            4
    # height           4
    # depth            4
    # colors           4
    # data length      4
    # data             N

    block = bytearray()

    # 3 = Front Cover
    block += struct.pack(
        ">I",
        3
    )

    mime_bytes = mime.encode(
        "ascii",
        errors="replace"
    )

    block += struct.pack(
        ">I",
        len(mime_bytes)
    )

    block += mime_bytes

    block += struct.pack(
        ">I",
        len(description_bytes)
    )

    block += description_bytes

    # 실제 이미지 크기
    width, height, depth, colors = get_image_info(
        data,
        mime
    )

    block += struct.pack(
        ">I",
        width
    )

    block += struct.pack(
        ">I",
        height
    )

    block += struct.pack(
        ">I",
        depth
    )

    block += struct.pack(
        ">I",
        colors
    )

    block += struct.pack(
        ">I",
        len(data)
    )

    block += data

    return bytes(block)


# ============================================================
# Ogg CRC32
#
# Ogg uses CRC polynomial 0x04C11DB7
# ============================================================

def make_ogg_crc_table():

    table = []

    for i in range(256):

        r = i << 24

        for _ in range(8):

            if r & 0x80000000:

                r = (
                    (r << 1)
                    ^ 0x04C11DB7
                ) & 0xFFFFFFFF

            else:

                r = (
                    r << 1
                ) & 0xFFFFFFFF

        table.append(r)

    return table


OGG_CRC_TABLE = make_ogg_crc_table()


def ogg_crc(data):

    crc = 0

    for byte in data:

        index = (
            ((crc >> 24) & 0xFF)
            ^ byte
        )

        crc = (
            (
                crc << 8
            )
            ^ OGG_CRC_TABLE[index]
        ) & 0xFFFFFFFF

    return crc


# ============================================================
# Ogg Page parser
# ============================================================

def parse_ogg_pages(data):

    pages = []

    offset = 0

    while offset < len(data):

        if data[offset:offset + 4] != b"OggS":

            raise ValueError(
                f"Invalid Ogg capture pattern at {offset}"
            )

        if offset + 27 > len(data):

            raise ValueError(
                "Truncated Ogg page header"
            )

        version = data[offset + 4]

        if version != 0:

            raise ValueError(
                "Unsupported Ogg version"
            )

        header_type = data[offset + 5]

        granule_position = struct.unpack_from(
            "<Q",
            data,
            offset + 6
        )[0]

        serial = struct.unpack_from(
            "<I",
            data,
            offset + 14
        )[0]

        sequence = struct.unpack_from(
            "<I",
            data,
            offset + 18
        )[0]

        checksum = struct.unpack_from(
            "<I",
            data,
            offset + 22
        )[0]

        segment_count = data[offset + 26]

        header_end = (
            offset
            + 27
            + segment_count
        )

        if header_end > len(data):

            raise ValueError(
                "Truncated Ogg segment table"
            )

        lacing = data[
            offset + 27:
            header_end
        ]

        payload_size = sum(lacing)

        page_end = (
            header_end
            + payload_size
        )

        if page_end > len(data):

            raise ValueError(
                "Truncated Ogg payload"
            )

        pages.append(
            {
                "offset": offset,
                "end": page_end,
                "header_type": header_type,
                "granule": granule_position,
                "serial": serial,
                "sequence": sequence,
                "checksum": checksum,
                "lacing": bytes(lacing),
                "payload": data[
                    header_end:page_end
                ],
            }
        )

        offset = page_end

    return pages


# ============================================================
# Ogg page 만들기
# ============================================================

def build_ogg_page(
    serial,
    sequence,
    header_type,
    granule,
    payload,
    lacing
):

    if len(lacing) > 255:

        raise ValueError(
            "Too many Ogg segments"
        )

    if len(payload) > 65025:

        raise ValueError(
            "Ogg page payload too large"
        )

    header = bytearray()

    header += b"OggS"

    # version
    header.append(0)

    # header type
    header.append(
        header_type
    )

    # granule position
    header += struct.pack(
        "<Q",
        granule
    )

    # serial
    header += struct.pack(
        "<I",
        serial
    )

    # sequence
    header += struct.pack(
        "<I",
        sequence
    )

    # CRC placeholder
    header += b"\x00\x00\x00\x00"

    # segment count
    header.append(
        len(lacing)
    )

    # lacing table
    header += bytes(lacing)

    page = (
        bytes(header)
        + payload
    )

    crc = ogg_crc(page)

    page = (
        page[:22]
        + struct.pack(
            "<I",
            crc
        )
        + page[26:]
    )

    return page


# ============================================================
# Ogg packet 추출
#
# 두 번째 packet = OpusTags
# ============================================================

def find_opus_tags_pages(data):

    pages = parse_ogg_pages(data)

    if not pages:

        raise ValueError(
            "No Ogg pages"
        )

    # 첫 번째 packet은 OpusHead
    packet_index = 0

    current_packet = bytearray()

    tag_start_page = None
    tag_end_page = None

    packets = []

    for page_index, page in enumerate(pages):

        payload_offset = 0

        for lace in page["lacing"]:

            current_packet += page["payload"][
                payload_offset:
                payload_offset + lace
            ]

            payload_offset += lace

            # lace < 255 means packet ends
            if lace < 255:

                packets.append(
                    (
                        packet_index,
                        bytes(current_packet),
                        page_index
                    )
                )

                if packet_index == 1:

                    if tag_start_page is None:

                        # packet started on the first
                        # page where we encountered it.
                        # For normal Opus this is this page.
                        tag_start_page = page_index

                    tag_end_page = page_index

                    return (
                        pages,
                        packets[0],
                        packets[1],
                        tag_start_page,
                        tag_end_page
                    )

                packet_index += 1
                current_packet.clear()

    raise ValueError(
        "OpusTags packet not found"
    )


# ============================================================
# OpusTags packet 생성
# ============================================================

def build_opus_tags_packet(
    tags,
    cover
):

    # Vendor 문자열은 넣지 않음
    vendor = b""

    comments = []

    for name, value in tags:

        name = name.strip()

        if not name:
            continue

        if "=" in name:
            continue

        if "\x00" in name:
            continue

        if "\r" in name:
            continue

        if "\n" in name:
            continue

        comment = (
            name
            + "="
            + value
        )

        comments.append(
            comment.encode("utf-8")
        )

    # --------------------------------------------------------
    # Cover
    # --------------------------------------------------------

    if cover:

        picture = build_picture_block(
            cover
        )

        if picture:

            encoded = base64.b64encode(
                picture
            ).decode("ascii")

            comments.append(
                (
                    "METADATA_BLOCK_PICTURE="
                    + encoded
                ).encode("ascii")
            )

    packet = bytearray()

    packet += b"OpusTags"

    # vendor length
    packet += struct.pack(
        "<I",
        len(vendor)
    )

    packet += vendor

    # user comment list length
    packet += struct.pack(
        "<I",
        len(comments)
    )

    for comment in comments:

        packet += struct.pack(
            "<I",
            len(comment)
        )

        packet += comment

    return bytes(packet)


# ============================================================
# Ogg에서 OpusTags 교체
# ============================================================

def replace_opus_tags(
    opus_path,
    tags,
    cover
):

    data = Path(opus_path).read_bytes()

    (
        pages,
        head_packet,
        old_tags_packet,
        tag_start_page,
        tag_end_page
    ) = find_opus_tags_pages(data)

    if not head_packet[1].startswith(
        b"OpusHead"
    ):

        raise ValueError(
            "First Ogg packet is not OpusHead"
        )

    if not old_tags_packet[1].startswith(
        b"OpusTags"
    ):

        raise ValueError(
            "Second Ogg packet is not OpusTags"
        )

    new_tags_packet = build_opus_tags_packet(
        tags,
        cover
    )

    # --------------------------------------------------------
    # OpusTags는 일반적으로 page boundary에서
    # 시작한다. FFmpeg 생성 Ogg도 이 구조다.
    # --------------------------------------------------------

    if tag_start_page != tag_end_page:

        raise ValueError(
            "Existing OpusTags spans multiple pages; "
            "unsupported layout"
        )

    old_page = pages[tag_start_page]

    # 해당 페이지가 OpusTags만 포함하는지 확인
    if len(old_page["lacing"]) == 0:

        raise ValueError(
            "Invalid OpusTags page"
        )

    # OpusTags packet이 page 처음부터 시작하는지 검사.
    # 첫 번째 lace부터 packet 종료까지가 Tags여야 한다.
    first_packet_size = 0

    for lace in old_page["lacing"]:

        first_packet_size += lace

        if lace < 255:
            break

    if first_packet_size != len(
        old_tags_packet[1]
    ):

        raise ValueError(
            "OpusTags is not isolated on its page"
        )

    serial = old_page["serial"]

    old_sequence = old_page["sequence"]

    # --------------------------------------------------------
    # 새 Tags packet을 Ogg pages로 분할
    # --------------------------------------------------------

    new_pages = []

    pos = 0

    sequence = old_sequence

    first_comment_page = True

    while pos < len(new_tags_packet):

        remaining = len(new_tags_packet) - pos

        # 한 페이지에는 최대 255개의 segment가 들어간다.
        #
        # 마지막 segment가 255이면 "packet이 다음 page에도 계속된다"
        # 는 의미이므로, packet이 끝나는 page에서는 종료용
        # 0~254 lacing 값을 반드시 넣어야 한다.
        #
        # 따라서 continuation page는 최대 254개의 255-byte
        # segment만 사용한다.
        max_payload = 254 * 255

        if remaining > max_payload:

            chunk_size = max_payload

            chunk = new_tags_packet[
                pos:
                pos + chunk_size
            ]

            # packet이 다음 page로 계속됨
            lacing = [255] * 254

        else:

            chunk_size = remaining

            chunk = new_tags_packet[
                pos:
                pos + chunk_size
            ]

            full_segments, remainder = divmod(
                len(chunk),
                255
            )

            lacing = [255] * full_segments

            # packet 종료
            #
            # remainder가 있으면 그 길이 자체가 종료 segment.
            # 정확히 255의 배수라면 0-byte 종료 segment가 필요하다.
            if remainder:
                lacing.append(remainder)
            else:
                lacing.append(0)

        # 첫 page는 일반 page,
        # 이후 continuation page는 0x01
        if first_comment_page:
            header_type = 0
        else:
            header_type = 0x01

        page = build_ogg_page(
            serial=serial,
            sequence=sequence,
            header_type=header_type,
            granule=0,
            payload=chunk,
            lacing=lacing
        )

        new_pages.append(page)

        sequence += 1
        pos += chunk_size

        first_comment_page = False

    # --------------------------------------------------------
    # 원본 pages 재조립
    #
    # 첫 번째 OpusHead page 유지
    # 새 OpusTags pages 삽입
    # 이후 audio pages 유지
    # sequence number / CRC만 재생성
    # --------------------------------------------------------

    output_pages = []

    # OpusHead page
    output_pages.append(
        data[
            pages[0]["offset"]:
            pages[0]["end"]
        ]
    )

    # 새 OpusTags
    output_pages.extend(
        new_pages
    )

    # --------------------------------------------------------
    # 기존 audio pages
    # --------------------------------------------------------

    next_sequence = (
        sequence
    )

    for page_index in range(
        tag_end_page + 1,
        len(pages)
    ):

        page = pages[page_index]

        # 기존 page의 payload/lacing은 그대로 사용
        # sequence와 CRC만 다시 만든다.

        rebuilt = build_ogg_page(
            serial=page["serial"],
            sequence=next_sequence,
            header_type=page["header_type"],
            granule=page["granule"],
            payload=page["payload"],
            lacing=page["lacing"]
        )

        output_pages.append(
            rebuilt
        )

        next_sequence += 1

    Path(opus_path).write_bytes(
        b"".join(output_pages)
    )


# ============================================================
# WebM → Ogg Opus
# ============================================================


def convert_file(
    source,
    output
):

    print()
    print("=" * 70)
    print(
        f"FILE : {source.name}"
    )

    # --------------------------------------------------------
    # BitChord WebM metadata 추출
    # --------------------------------------------------------

    tags, attachments = extract_metadata(source)

    cover = find_cover(attachments)

    print(f"TAG  : {len(tags)}")

    for name, value in tags:

        preview = (
            value
            .replace("\r", "\\r")
            .replace("\n", "\\n")
        )

        if len(preview) > 100:
            preview = preview[:97] + "..."

        print(f"       {name} = {preview}")

    print(
        "COVER:",
        "YES" if cover else "NO"
    )

    temp_output = output.with_name(
        output.stem + ".tmp.opus"
    )

    if temp_output.exists():
        temp_output.unlink()

    # --------------------------------------------------------
    # Album Art 임시 파일
    #
    # 긴 base64 문자열을 FFmpeg 인자로 직접 전달하지 않는다.
    # --------------------------------------------------------

    cover_file = None

    try:

        if cover:

            cover_file = output.with_name(
                output.stem + ".cover.tmp"
            )

            cover_file.write_bytes(cover["data"])

        # ----------------------------------------------------
        # WebM Opus -> Ogg Opus
        #
        # -c:a copy
        # = Opus 재인코딩 없음
        # ----------------------------------------------------

        command = [
            "ffmpeg",
            "-hide_banner",
            "-loglevel",
            "error",
            "-y",

            "-i",
            str(source),
        ]

        command.extend([
            "-map",
            "0:a:0",

            "-map_metadata",
            "-1",

            "-c:a",
            "copy",
        ])

        # ----------------------------------------------------
        # Metadata
        # ----------------------------------------------------

        for name, value in tags:

            if not name:
                continue

            command.extend([
                "-metadata",
                f"{name}={value}",
            ])

        # ----------------------------------------------------
        # Album Art
        #
        # FFmpeg가 cover 파일을 읽어서
        # METADATA_BLOCK_PICTURE을 생성하도록 한다.
        # ----------------------------------------------------

        command.extend([
            "-f",
            "opus",

            str(temp_output),
        ])

        subprocess.run(
            command,
            check=True
        )

        # OpusTags에 BitChord 메타데이터와 커버 삽입
        replace_opus_tags(
            temp_output,
            tags,
            cover
        )

        os.replace(
            temp_output,
            output
        )

        print(
            f"OK   : {output}"
        )

        return True

    except Exception as e:

        print(
            f"FAIL : {e!r}",
            file=sys.stderr
        )

        if temp_output.exists():
            temp_output.unlink()

        return False

    finally:

        if cover_file and cover_file.exists():

            cover_file.unlink()


# ============================================================
# Main
# ============================================================

def main():

    if len(sys.argv) != 2:

        print(
            f"사용법: {sys.argv[0]} <WebM_폴더>"
        )

        print(
            f"예시: {sys.argv[0]} BitChord"
        )

        return 1

    source_dir = (
        Path(sys.argv[1])
        .expanduser()
        .resolve()
    )

    if not source_dir.is_dir():

        print(
            f"폴더가 없습니다: {source_dir}",
            file=sys.stderr
        )

        return 1

    # --------------------------------------------------------
    # converted
    # --------------------------------------------------------

    output_dir = (
        source_dir
        / "converted"
    )

    output_dir.mkdir(
        parents=True,
        exist_ok=True
    )

    # --------------------------------------------------------
    # WebM 목록
    # --------------------------------------------------------

    files = sorted(
        p
        for p in source_dir.iterdir()
        if p.is_file()
        and p.suffix.lower() == ".webm"
    )

    if not files:

        print(
            f".webm 파일이 없습니다: {source_dir}"
        )

        return 0

    print()
    print(
        "BitChord WebM → Ogg Opus"
    )
    print(
        "=========================================="
    )
    print(
        f"Source : {source_dir}"
    )
    print(
        f"Output : {output_dir}"
    )
    print(
        f"Files  : {len(files)}"
    )

    success = 0
    failed = 0

    # --------------------------------------------------------
    # 변환
    # --------------------------------------------------------

    for source in files:

        output = (
            output_dir
            / f"{source.stem}.opus"
        )

        if convert_file(
            source,
            output
        ):

            success += 1

        else:

            failed += 1

    # --------------------------------------------------------
    # 결과
    # --------------------------------------------------------

    print()
    print(
        "=" * 70
    )

    print(
        f"완료: {success}개"
    )

    print(
        f"실패: {failed}개"
    )

    print(
        f"결과 폴더: {output_dir}"
    )

    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(
        main()
    )
