Skill: officialpm/apple-media-officialpm (version 0.1.1)

--- name: apple-media description: Discover and control Apple media/AirPlay devices (HomePod, Apple TV, AirPlay speakers) from macOS. Use when you want to scan for AirPlay devices, map names→IPs/IDs, pair/connect, and control playback/volume using pyatv (atvremote) and Airfoil. --- # Apple Media (AirPlay + Apple TV control) **Author:** Parth Maniar — [@officialpm](https://github.com/officialpm) This skill is a thin workflow wrapper around two tools: - **pyatv** (`atvremote`) for discovering Apple TVs/HomePods and (when supported/paired) remote-control style commands. - **Airfoil** (via the existing `airfoil` skill) for reliable **speaker connect/disconnect + volume** control across AirPlay speakers (including HomePods). ## Setup This skill uses `pyatv` installed via **pipx**. Install/repair (pinned to Python 3.12 to avoid Python 3.14 asyncio issues): ```bash pipx install pyatv || pipx upgrade pyatv pipx reinstall pyatv --python python3.12 ``` Verify: ```bash atvremote --help | head ``` ## Quick start ### 1) Scan the network for devices ```bash # Fast scan (5s) ./scripts/scan.sh 5 # Faster scan when you know IP(s) ./scripts/scan-hosts.sh "10.0.0.28,10.0.0.111" 3 # Or JSON output node ./scripts/scan-json.js 5 ``` You’ll see devices like: - HomePods (e.g., "Living Room", "Bedroom") - Apple TV - AirPlay-capable TVs ### 2) Control HomePod / speaker volume (recommended path) Use Airfoil for speaker control (reliable for HomePods): ```bash # List speakers Airfoil can see ../airfoil/airfoil.sh list # Connect and set volume ./scripts/connect.sh "Living Room" ./scripts/volume.sh "Living Room" 35 # Disconnect (direct) ../airfoil/airfoil.sh disconnect "Living Room" ``` ### 3) Apple TV remote commands (pyatv) First, scan to find the Apple TV name or id, then run commands: ```bash # Examples (device name can be Apple TV or other targets) atvremote -n "TV" playing atvremote -n "TV" play_pause atvremote -n "TV" turn_on atvremote -n "TV" turn_off ``` If you get auth/protocol errors, pairing/credentials are needed (device-dependent). ## Notes / gotchas - **pyatv HomePod control often requires authentication** and may not support all remote-control commands out of the box. - When pyatv fails for HomePod playback/volume, prefer **Airfoil** for volume + speaker routing. - `atvremote scan` is the source of truth for IP/ID discovery. ## Bundled scripts ### `scripts/scan.sh` Runs `atvremote scan` with a configurable timeout. ```bash ./scripts/scan.sh 5 ``` ### `scripts/scan-json.js` Parses `atvremote scan` output into a compact JSON summary (name, address, model, services). ```bash node ./scripts/scan-json.js ```

path: CHANGELOG.md
content: # Changelog

## 0.1.1 — 2026-01-28
- Add `scan-hosts.sh` for faster targeted scanning via `atvremote --scan-hosts`.

## 0.1.0 — 2026-01-28
- Initial release of `apple-media` skill: AirPlay device scanning (pyatv) + speaker control wrappers (Airfoil).


path: README.md
content: # apple-media

Discover and control AirPlay / Apple media devices from macOS.

This repo contains the **Clawdbot skill** located in `SKILL.md` plus helper scripts under `scripts/`.

## Author

Parth Maniar — [@officialpm](https://github.com/officialpm)

## What it does

- **Scan** the local network for AirPlay devices (HomePod, Apple TV, AirPlay TVs)
- Provide a **JSON** summary of discovered devices
- Delegate **speaker volume/routing** control to Airfoil (reliable for HomePods)
- Use **pyatv** (`atvremote`) for Apple TV / supported device control

## Install / setup

### pyatv (recommended via pipx)

```bash
brew install pipx
pipx install pyatv || pipx upgrade pyatv

# Pin to Python 3.12 to avoid Python 3.14 asyncio issues
pipx reinstall pyatv --python python3.12
```

Verify:

```bash
atvremote --help | head
```

### Airfoil (optional but recommended for HomePods)

Install Airfoil and grant Accessibility permissions.

- `brew install --cask airfoil`

## Usage

### Scan (text)

```bash
./scripts/scan.sh 5
```

### Scan (JSON)

```bash
node ./scripts/scan-json.js 5
```

### Faster scan (known IPs)

```bash
./scripts/scan-hosts.sh "10.0.0.28,10.0.0.111" 3
```

### Control HomePod volume (Airfoil)

```bash
# Requires Airfoil + the Clawdbot airfoil skill scripts
./scripts/connect.sh "Living Room"
./scripts/volume.sh "Living Room" 35
```

## Privacy / safety

- This repo intentionally avoids committing any personal data (no resume paths, no tokens, no device credentials).
- The scan output includes IP addresses at runtime; **do not commit scan outputs**.


path: scripts/connect.sh
content: #!/usr/bin/env bash
set -euo pipefail

SPEAKER="${1:?speaker name required}"

# Delegate to the existing Airfoil skill.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AIRFOIL_SH="$SCRIPT_DIR/../../airfoil/airfoil.sh"

"$AIRFOIL_SH" connect "$SPEAKER"


path: scripts/scan-hosts.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Faster scan when you know IP(s)
# Usage: scan-hosts.sh <ip[,ip...]> [timeout]
HOSTS="${1:?hosts (ip or ip,ip) required}"
TIMEOUT="${2:-3}"

atvremote --scan-hosts "$HOSTS" -t "$TIMEOUT" scan


path: scripts/scan-json.js
content: #!/usr/bin/env node
/*
Parse `atvremote scan` output into a compact JSON.
Works with pyatv/atvremote 0.17.x default scan formatting.
*/

const { execSync } = require('child_process')

function runScan(timeoutSec = 5) {
  const out = execSync(`atvremote scan -t ${Number(timeoutSec)}`, { encoding: 'utf8' })
  return out
}

function parseScan(text) {
  const lines = text.split(/\r?\n/)
  const devices = []

  let cur = null
  let inServices = false
  let inIdentifiers = false

  for (const raw of lines) {
    const line = raw.trim()

    // New device starts with "Name:"
    const nameMatch = line.match(/^Name:\s*(.+)$/)
    if (nameMatch) {
      if (cur) devices.push(cur)
      cur = { name: nameMatch[1].trim(), address: null, model: null, os: null, ids: [], services: [] }
      inServices = false
      inIdentifiers = false
      continue
    }

    if (!cur) continue

    const modelMatch = line.match(/^Model\/SW:\s*(.+)$/)
    if (modelMatch) {
      cur.model = modelMatch[1].trim()
      continue
    }

    const addrMatch = line.match(/^Address:\s*(.+)$/)
    if (addrMatch) {
      cur.address = addrMatch[1].trim()
      continue
    }

    if (line.startsWith('Identifiers:')) {
      inIdentifiers = true
      inServices = false
      continue
    }
    if (line.startsWith('Services:')) {
      inServices = true
      inIdentifiers = false
      continue
    }

    if (inIdentifiers) {
      const idMatch = line.match(/^-\s*(.+)$/)
      if (idMatch) cur.ids.push(idMatch[1].trim())
      continue
    }

    if (inServices) {
      // Example:
      // - Protocol: AirPlay, Port: 7000, Credentials: None, Requires Password: False, Password: None, Pairing: Disabled
      const svcMatch = line.match(/^-\s*Protocol:\s*([^,]+),\s*Port:\s*(\d+),\s*Credentials:\s*([^,]+),\s*Requires [REDACTED_SECRET],]+),\s*[REDACTED_SECRET],]+),\s*Pairing:\s*(.+)$/)
      if (svcMatch) {
        cur.services.push({
          protocol: svcMatch[1].trim(),
          port: Number(svcMatch[2]),
          credentials: svcMatch[3].trim(),
          requiresPassword: svcMatch[4].trim(),
          [REDACTED_SECRET]),
          pairing: svcMatch[6].trim(),
        })
      }
      continue
    }
  }

  if (cur) devices.push(cur)
  return devices
}

const timeout = process.argv[2] ? Number(process.argv[2]) : 5
const raw = runScan(timeout)
const devices = parseScan(raw)
console.log(JSON.stringify({ scannedAt: new Date().toISOString(), timeoutSec: timeout, devices }, null, 2))


path: scripts/scan.sh
content: #!/usr/bin/env bash
set -euo pipefail

TIMEOUT="${1:-5}"

# atvremote uses -t / --scan-timeout
atvremote scan -t "$TIMEOUT"


path: scripts/volume.sh
content: #!/usr/bin/env bash
set -euo pipefail

SPEAKER="${1:?speaker name required}"
VOL="${2:?volume 0-100 required}"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AIRFOIL_SH="$SCRIPT_DIR/../../airfoil/airfoil.sh"

"$AIRFOIL_SH" volume "$SPEAKER" "$VOL"


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: clean (LOW)