Metadata-Version: 2.4
Name: playdirector
Version: 0.2.0
Summary: Async Python library for PS4/PS5 second-screen control (wake, standby, button input)
Project-URL: Homepage, https://github.com/jackjpowell/playdirector
Project-URL: Repository, https://github.com/jackjpowell/playdirector
Project-URL: Issues, https://github.com/jackjpowell/playdirector/issues
Project-URL: Changelog, https://github.com/jackjpowell/playdirector/releases
License: MIT
Keywords: asyncio,playstation,ps4,ps5,remote-control
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Home Automation
Requires-Python: >=3.11
Requires-Dist: aiohttp>=3.9
Requires-Dist: cryptography>=41.0
Description-Content-Type: text/markdown

# playdirector

An async Python library for controlling PS4 and PS5 consoles over your local network — wake from standby, send button inputs, navigate home, and more.

[![PyPI version](https://img.shields.io/pypi/v/playdirector)](https://pypi.org/project/playdirector/)
[![Python](https://img.shields.io/pypi/pyversions/playdirector)](https://pypi.org/project/playdirector/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

## Features

- **Discover** PS4/PS5 consoles on your local network via UDP broadcast
- **Wake** a console from standby without a full session
- **Connect** and send button inputs (PS4), navigate to home (PS5)
- **Pair** with a PS5 using your PSN credentials (NPSSO token)
- **Credential storage** — JSON files persisted to `~/.config/playdirector/`
- Fully `async`/`await` — built on `asyncio` and `aiohttp`

## Requirements

- Python 3.11+
- PS4 (firmware ≥ 8.0) or PS5
- Console and computer on the same local network

## Installation

```bash
pip install playdirector
```

## Quick Start

```python
import asyncio
from playdirector import find, connect, RemoteOperation
from playdirector.credentials import JsonCredentialStorage

async def main():
    storage = JsonCredentialStorage()

    # Probe a known IP
    device = await find("192.168.1.50")
    if device is None:
        print("Console not found")
        return

    # Load previously paired credentials
    cred = storage.load(device.device_id)
    if cred is None:
        print("No credentials — run pairing first")
        return

    # Connect and send the PS button (PS4)
    async with connect(device, cred) as session:
        await session.send_keys([RemoteOperation.PS])

asyncio.run(main())
```

## Pairing

Before connecting you need to pair with the console once to obtain credentials.
The `pair()` function detects whether the console is a PS4 or PS5 and runs the correct flow automatically.

You'll need:
- Your [NPSSO token](https://ca.account.sony.com/api/v1/ssocookie) from the PSN website
- The 8-digit PIN shown on the console:
  - **PS5** — Settings → System → Remote Play → Link Device
  - **PS4** — Settings → Remote Play Connection Settings → Add Device

```python
import asyncio
from playdirector import pair
from playdirector.credentials import JsonCredentialStorage

async def main():
    cred = await pair(
        ip="192.168.1.50",
        pin="12345678",      # 8-digit PIN from console
        npsso="your-npsso", # from https://ca.account.sony.com/api/v1/ssocookie
    )
    JsonCredentialStorage().save(cred)
    print("Paired:", cred.device_id)

asyncio.run(main())
```

## API

### Discovery

```python
from playdirector import scan, find

# Scan the network — yields all responding devices
async for device in scan(timeout=5):
    print(device.device_type, device.ip, device.status)

# Probe a specific IP — returns one device or None
device = await find("192.168.1.50")
```

`DiscoveredDevice` fields:

| Field | Type | Description |
| --- | --- | --- |
| `ip` | `str` | IP address |
| `name` | `str` | Console name |
| `device_type` | `DeviceType` | `PS4` or `PS5` |
| `status` | `DeviceStatus` | `AWAKE` or `STANDBY` |
| `device_id` | `str` | Unique device identifier |
| `running_app_name` | `str \| None` | Currently running app (if awake) |
| `running_app_titleid` | `str \| None` | Title ID of running app |

### Control

```python
from playdirector import find, wake, connect, standby, send_buttons, go_home
from playdirector.packets import RemoteOperation

device = await find("192.168.1.50")

# Wake from standby (UDP only — no session, call before connecting)
await wake(device, cred)

# Put into standby (PS4 + PS5)
await standby(device, cred)

# Send button inputs (PS4 only)
await send_buttons(device, cred, [RemoteOperation.PS])
await send_buttons(device, cred, [RemoteOperation.UP, RemoteOperation.ENTER])

# Navigate PS5 to home screen (PS5 only)
await go_home(device, cred)

# Open a session manually for multiple operations without reconnecting
async with connect(device, cred) as session:
    await session.send_keys([RemoteOperation.UP])
    await session.send_keys([RemoteOperation.ENTER])
    await session.standby()
```

> **Note:** `send_buttons` is supported on **PS4 only**. The PS5 Remote Play protocol does not expose button input — only `wake()`, `standby()`, and `go_home()` are available for PS5.

### Available Buttons (`RemoteOperation`) — PS4 only

`UP` · `DOWN` · `LEFT` · `RIGHT` · `ENTER` · `BACK` · `OPTION` · `PS` · `CANCEL`

Multiple buttons can be combined: `RemoteOperation.UP | RemoteOperation.LEFT`

### Credential Storage

```python
from playdirector.credentials import JsonCredentialStorage

storage = JsonCredentialStorage()           # defaults to ~/.config/playdirector/
storage = JsonCredentialStorage("/my/dir") # custom path

storage.save(cred)                         # saves to <dir>/<device_id>.json
cred = storage.load(device_id)             # returns None if not found
storage.delete(device_id)
ids = storage.list_device_ids()
```

## Error Handling

```python
from playdirector import PlayActorError, LoginError, UnsupportedDeviceError

try:
    async with connect(device, cred) as session:
        ...
except LoginError:
    print("Bad credentials")
except UnsupportedDeviceError:
    print("Operation not supported on this console")
except PlayActorError as e:
    print("Protocol error:", e)
```

## License

MIT — see [LICENSE](LICENSE).
