#!/usr/bin/env python3
"""diy-babycam-devices — find the microphone and webcam to use.

  diy-babycam-devices audio   print the PipeWire/PulseAudio source name
  diy-babycam-devices video   print the /dev/videoN capture node
  diy-babycam-devices list    show everything found; * marks the pick

USB devices are preferred, since that's what a baby cam is. Exits 1
(printing nothing) when no device is found.
"""

import fcntl
import glob
import json
import os
import re
import subprocess
import sys

VIDIOC_QUERYCAP = 0x80685600  # _IOR('V', 0, struct v4l2_capability)
V4L2_CAP_VIDEO_CAPTURE = 0x1
V4L2_CAP_VIDEO_CAPTURE_MPLANE = 0x1000
V4L2_CAP_DEVICE_CAPS = 0x80000000


def audio_sources():
    """[(source name, description)], best first."""
    try:
        out = subprocess.run(["pw-dump"], capture_output=True, text=True,
                             timeout=5, check=True).stdout
        objs = json.loads(out)
    except (OSError, subprocess.SubprocessError, ValueError):
        return []
    found = []
    for o in objs:
        if o.get("type") != "PipeWire:Interface:Node":
            continue
        props = o.get("info", {}).get("props", {})
        name = props.get("node.name", "")
        if props.get("media.class") != "Audio/Source" or name.endswith(".monitor"):
            continue
        found.append((name, props.get("node.description", name)))
    found.sort(key=lambda s: (".usb-" not in s[0], s[0]))
    return found


def video_devices():
    """[(/dev/videoN, card name)] for capture nodes, best first.

    A webcam usually exposes several nodes (capture, metadata, ...), so
    ask the driver which ones can actually capture video.
    """
    found = []
    paths = sorted(glob.glob("/dev/video*"),
                   key=lambda p: int(re.sub(r"\D", "", p) or 0))
    for path in paths:
        try:
            fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
        except OSError:
            continue
        try:
            buf = bytearray(104)
            fcntl.ioctl(fd, VIDIOC_QUERYCAP, buf)
        except OSError:
            continue
        finally:
            os.close(fd)
        card = bytes(buf[16:48]).split(b"\0")[0].decode(errors="replace")
        bus = bytes(buf[48:80]).split(b"\0")[0].decode(errors="replace")
        caps = int.from_bytes(buf[84:88], "little")
        if caps & V4L2_CAP_DEVICE_CAPS:
            caps = int.from_bytes(buf[88:92], "little")
        if caps & (V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_VIDEO_CAPTURE_MPLANE):
            found.append((not bus.startswith("usb-"), path, card))
    found.sort(key=lambda d: d[0])  # stable: USB first, then by number
    return [(path, card) for _, path, card in found]


def main():
    what = sys.argv[1] if len(sys.argv) > 1 else "list"
    audio, video = audio_sources(), video_devices()
    if what == "audio":
        print(audio[0][0]) if audio else sys.exit(1)
    elif what == "video":
        print(video[0][0]) if video else sys.exit(1)
    elif what == "list":
        print("Audio sources:")
        for i, (name, desc) in enumerate(audio):
            print(f"  {'*' if i == 0 else ' '} {name}  ({desc})")
        print("Video devices:")
        for i, (path, card) in enumerate(video):
            print(f"  {'*' if i == 0 else ' '} {path}  ({card})")
    else:
        sys.exit(__doc__)


if __name__ == "__main__":
    main()
