# Devices — shared media volume

**Job:** Present many physical disks as **one media library**, optimize placement
for **where you are watching**, and use spare laptop space as **extension** —
not a duplicate warmup cache.

One-liner: *RAID-like capacity across devices; retrieval follows the player.*

Parent: [devices.md](./devices.md). Engine:
[BitTorrentDrive internal face](../packages/bittorrentdrive.md). Consumed by
[library.md](./library.md) and Watch playback paths.

---

## Problem (today)

| Behavior | Issue |
|----------|-------|
| Canonical library on the central always-on host | Correct |
| **Offline warmup** rsyncs to `~/offline-cache` on laptop | **Duplicate** bytes |
| Library index keyed to one `mediaRoot` | Extension space invisible to catalog |
| Watching on laptop | Streams or plays **copy**, not unified namespace |

**Target:** A client machine contributes spare capacity → central host’s effective library grows by that amount. You watch on the client → episodes materialize on the extension tier; paths resolve locally without a second copy on the central host.
grows by 200 GB. You watch on the Mac → Show X episodes **materialize** on the
extension tier; paths resolve locally **without** a second copy on canonical disk.

---

## Concepts

| Term | Meaning |
|------|---------|
| **Logical volume** | Single namespace: `tv/Show/Season 01/E01.mkv` |
| **Storage device** | Registered device with `custody` and/or `extension` role |
| **Tier** | `canonical` (central always-on host) · `extension` (spare on client machines) · `cache` (ephemeral) |
| **Placement** | Which tier holds bytes for a `contentKey` |
| **Alias** | Library path resolves to nearest tier; no duplicate catalog entry |
| **Extended warmup** | Proactive placement on extension tier for show currently watched |
| **Retrieval optimizer** | On play: ensure local tier has object; else fetch/promote |

Not classical RAID-5 — **policy-driven replication + erasure optional later**.
MVP: **1 canonical + N extension replicas** with explicit quotas.

---

## Architecture

```mermaid
flowchart TB
    subgraph lib [Library pillar]
        Cat[Catalog index]
        CK[contentKey]
    end

    subgraph dev [Devices pillar]
        Reg[Device registry]
        Pool[Storage pool composer]
        Place[Placement index]
        Opt[Retrieval optimizer]
    end

    subgraph watch [Watch pillar]
        Play[PlayerController]
        Warm[Extended warmup]
    end

    subgraph dl [Download pillar]
        Custody[custody / re-pin]
    end

    Reg --> Pool
    Pool --> Place
    Cat --> CK
    CK --> Place
    Play --> Opt
    Opt --> Place
    Warm --> Place
    Custody --> Place
    Place -->|alias resolve| Play
```

---

## Storage pool config (planned schema)

```jsonc
{
  "volumeId": "home-media",
  "canonicalDeviceId": "central",
  "tiers": [
    {
      "deviceId": "central",
      "role": "canonical",
      "mountPath": "/media",
      "quotaBytes": null
    },
    {
      "deviceId": "laptop1",
      "role": "extension",
      "mountPath": "/Users/user/spare-media",
      "quotaBytes": 214748364800,
      "contributesToCanonical": true
    }
  ],
  "placementPolicy": {
    "minCanonicalCopies": 1,
    "extensionWarmupEpisodes": 3,
    "extensionWarmupShows": 2,
    "onPlayPromote": "extension-if-watching-device",
    "dedupeByContentKey": true
  }
}
```

`contributesToCanonical: true` — extension bytes count toward the central host’s
**effective** free space for library planning (operator mental model: “I expanded
the central storage”).

---

## Placement index (planned)

`~/.local/state/tv-anarchy/media-placement.json`:

```jsonc
{
  "contentKey-abc": {
    "logicalPath": "tv/Show/Season 01/E01.mkv",
    "locations": [
      { "deviceId": "central", "tier": "canonical", "path": "/media/tv/Show/...", "bytes": 450000000 },
      { "deviceId": "laptop1", "tier": "extension", "path": "/Users/user/spare/...", "bytes": 450000000, "aliasOf": "canonical" }
    ],
    "preferredForDevice": { "laptop1": "extension" }
  }
}
```

`aliasOf` — extension file is **hardlink, reflink, or bind-mount** to canonical
when co-located; remote extension uses rsync **once** then inode-stable slot.

---

## Extended warmup (replaces duplicate offline cache)

| Step | Action |
|------|--------|
| 1 | User plays Show X on the extension device (e.g. laptop) |
| 2 | Optimizer marks show **active** on device |
| 3 | Warmup plans next N episodes (existing `OfflineCachePolicy` windows) |
| 4 | Placement targets **extension tier** on the local device, not a separate cache dir |
| 5 | `MediaPaths` / `DownloadsIndex` resolve logical path → extension path |
| 6 | If episode already on canonical only → **single fetch** to extension; catalog unchanged |
| 7 | On cull: drop extension copies first; canonical retained |

**No duplicate** — one `contentKey`, one catalog row, multiple **locations**.

---

## Retrieval optimizer

On `PlayerController.launch`:

1. Resolve `contentKey` → placement record.
2. If playing device has `extension` or `canonical` copy locally → `file://` path.
3. Else if canonical reachable → stream (mpv route) **and** optional background promote to extension.
4. Else → offline error / queue warmup.

Ranking: `local extension > local canonical > remote canonical > stream`.

---

## RAID / optimization roadmap

| Stage | Capability |
|-------|------------|
| **MVP** | 1 canonical + 1 extension tier; alias paths; extended warmup |
| **1b** | Multiple extension devices (desktop + NAS head) |
| **2** | Erasure-coded striping for cold archive (optional, seed devices) |
| **3** | Retrieval-driven re-pin integrates Download `custody` floor |

Download **custody** ensures canonical copies on always-on storage; Devices
**extension** handles opportunistic laptop spare — different jobs.

---

## UI (Devices pillar)

| Surface | Control |
|---------|---------|
| Devices → storage device | Enable extension pool, pick folder, quota slider |
| Devices → shared volume | Attach extension devices to canonical |
| This Mac | Extension usage bar, warmup queue (placement-aware) |
| Settings → Devices | Default warmup windows (shared with offline policy) |

---

## Implementation notes

| Component | Change |
|-----------|--------|
| `OfflineCacheController` | Delegate pin/fetch to `BitTorrentDrive.InternalPin` |
| `PlacementController` | Thin Devices policy layer over drive API |
| `DownloadsIndex` | Index logical + per-tier paths |
| `MediaPaths` | Resolve via placement index |
| `LibraryScanner` | Scan canonical root; extension tiers register via placement |
| Governor `keeper` | Warmup targets extension mount on active device |
| `DeviceConfig` | `extensionPool: ExtensionPoolConfig?` |

---

## Risks

| Risk | Mitigation |
|------|------------|
| Path drift vs contentKey | Library pillar owns keys; placement references keys only |
| Partial rsync | Keep `--append-verify`; placement `state: pending` |
| Cross-platform links | macOS APFS reflink; Linux bind mount; fallback copy + hash verify |
| Quota oversubscription | `quotaBytes` enforced per tier before promote |

---

## Tests (planned)

| Test | Asserts |
|------|---------|
| `PlacementIndexTests` | Add location, resolve nearest |
| `ExtendedWarmupTests` | Play show → plan targets extension not duplicate dir |
| `MediaPathsPlacementTests` | Alias path on laptop |
| `StoragePoolConfigTests` | Decode pool JSON |

---

## Exit criteria (MVP)

- [ ] Operator assigns spare folder on a client as extension of the central volume
- [ ] Watching on laptop warms next episodes to extension without second catalog entry
- [ ] Player opens extension path via alias, not `offline-cache` duplicate
- [ ] Cull frees extension first; canonical on the central host untouched