# TorrServer — P2P Torrent Streaming Engine

## Installation

### Direct Binary (preferred — Docker has version issues)
```bash
wget "https://github.com/YouROK/TorrServer/releases/latest/download/TorrServer-linux-amd64" -O TorrServer-linux-amd64
chmod +x TorrServer-linux-amd64
./TorrServer-linux-amd64 --port 8090
```

### Docker (alternative)
```bash
docker run -d --name torrserver -p 8090:8090 --restart unless-stopped yourok/torrserver:latest
```
**Gotcha**: The Docker image sometimes extracts the binary with a `.7` extension. Binary download directly is more reliable.

## API Reference (MatriX fork)

**CRITICAL GOTCHA**: The MatriX fork (from GitHub releases) uses **bare** `/torrents` endpoint, NOT `/api/torrents`. The official TorrServer docs at deepwiki.com document `/api/torrents` but the actual binary uses the un-prefixed path.

| Action | Method | URL | Body |
|--------|--------|-----|------|
| Add magnet | POST | `/torrents` | `{"action":"add","link":"magnet:...","save_to_db":true}` |
| List all | POST | `/torrents` | `{"action":"list"}` |
| Get single | POST | `/torrents` | `{"action":"get","hash":"HASH"}` |
| Remove | POST | `/torrents` | `{"action":"rem","hash":"HASH"}` |
| Delete permanently | POST | `/torrents` | `{"action":"rem","hash":"HASH"}` |
| Health | GET | `/echo` | — |
| Stream file | GET | `/stream/{hash}/{file_id}` | — |
| Play (auto-preload) | GET | `/play/{hash}/{file_id}` | — |

**IMPORTANT**: The streaming URLs use PATH format, NOT query params:
- ✅ Correct: `/play/d7a46713eaee18c746b3/1`
- ❌ Wrong: `/play?hash=d7a46713eaee18c746b3&file_id=1` (returns 404)

### Stream /torrents/ vs /api/torrents/ confusion
- Official docs say `/api/torrents` — but the actual binary downloaded from GitHub releases uses `/torrents`
- If you get 405 Method Not Allowed, you're using the wrong path
- Always test with `curl -sI http://host:port/echo` first to verify the server is running
- The `/api/` prefix is for a different fork — this one is MatriX (version string contains "MatriX")
- Check version: `curl http://host:port/echo` — if it returns "MatriX.X.XX", you need the bare `/torrents` path

## TorrentStatus Object
```json
{
  "stat": 1,
  "stat_string": "Torrent getting info",
  "hash": "d7a46713...",
  "name": "Torrent Name",
  "title": "Display Title",
  "total_peers": 1,
  "file_stats": [],
  "poster": "",
  "category": ""
}
```
- `stat=0`: Not started
- `stat=1`: Getting info (connecting to peers, downloading metadata)
- `stat=2`: Working (ready to stream)
- `file_stats` is empty until stat=2 — stream endpoints return 400/500 until populated
- Torrents with 0 seeders will hang at stat=1 forever

## Flask Proxy for CORS-Free Streaming

```python
@app.route('/torrent/play')
def torrent_play():
    hash_val = request.args.get('hash', '')
    file_id = request.args.get('file_id', '0')
    if not hash_val or len(hash_val) < 10:
        return 'Missing hash', 400
    
    # Ensure torrent is added to TorrServer
    try:
        magnet = f'magnet:?xt=urn:btih:{hash_val}'
        req = urllib.request.Request(
            f'http://{TS_HOST}:{TS_PORT}/torrents',  # NOTE: bare path, NOT /api/torrents (MatriX fork)
            data=json.dumps({"action":"add","link":magnet,"save_to_db":True}).encode(),
            headers={'Content-Type':'application/json'})
        urllib.request.urlopen(req, timeout=10)
    except: pass  # May already exist
    
    # Try both stream and play endpoints
    for ts_url in [
        f'http://{TS_HOST}:{TS_PORT}/play/{hash_val}/{file_id}',
        f'http://{TS_HOST}:{TS_PORT}/stream/{hash_val}/{file_id}',
    ]:
        try:
            ts_req = urllib.request.Request(ts_url, headers={'User-Agent': 'Mozilla/5.0'})
            ts_resp = urllib.request.urlopen(ts_req, timeout=30)
            return Response(iter(lambda: ts_resp.read(65536), b''), status=200, headers={
                'Content-Type': ts_resp.headers.get('Content-Type', 'video/mp4'),
                'Accept-Ranges': 'bytes',
                'Access-Control-Allow-Origin': '*',
            })
        except urllib.error.HTTPError as he:
            if he.code == 404: continue
            return f'TorrServer error: {he.code}', 502
        except: continue
    
    return 'TorrServer could not open stream', 502
```

## Torrent Status API (for JS polling)

```python
@app.route('/api/torrent_status')
def torrent_status():
    hash_val = request.args.get('hash', '')
    if not hash_val: return {'error': 'no hash'}, 400
    try:
        req = urllib.request.Request(
            f'http://{TS_HOST}:{TS_PORT}/torrents',  # bare path, NOT /api/torrents
            data=json.dumps({"action":"get","hash":hash_val}).encode(),
            headers={'Content-Type':'application/json'})
        r = urllib.request.urlopen(req, timeout=5)
        data = json.loads(r.read())
        if isinstance(data, list): data = data[0] if data else {}
        return {
            'stat': data.get('stat', 0),
            'stat_string': data.get('stat_string', 'Unknown'),
            'files': len(data.get('file_stats', [])),
            'peers': data.get('total_peers', 0),
        }
    except Exception as e:
        return {'error': str(e)}, 502
```

## P2P Playback JavaScript Pattern

```javascript
function playP2P(url, hash) {
  fetch(url, {method: 'HEAD'}).catch(function(){});
  
  var check = setInterval(function() {
    fetch('/api/torrent_status?hash=' + hash).then(function(r) { return r.json(); }).then(function(d) {
      if (d.stat_string === 'Working' && d.files > 0) {
        clearInterval(check);
        var v = document.createElement('video');
        v.controls = true; v.autoplay = true;
        v.innerHTML = '<source src="' + url + '" type="video/mp4">';
        playerDiv.innerHTML = '';
        playerDiv.appendChild(v);
      } else {
        // Update status text with stat_string + pollCount
        statusEl.textContent = d.stat_string + ' (' + pollCount + 's)';
      }
    });
    fetch(url, {method: 'HEAD'}).then(function(r) {
      if (r.ok || r.status === 206) {
        clearInterval(check);
        var v = document.createElement('video');
        v.controls = true; v.autoplay = true;
        v.innerHTML = '<source src="' + url + '" type="video/mp4">';
        playerDiv.innerHTML = '';
        playerDiv.appendChild(v);
      }
    });
  }, 1000);
}
```

## Common Issues
- **Binary fails to start**: Need `libc6` and standard Linux x86_64 deps. Check with `ldd TorrServer-linux-amd64`
- **Docker container restarts in loop**: Check logs with `docker logs torrserver`. Usually a binary extraction issue.
- **Stream returns 400**: Torrent metadata not downloaded yet. Wait for stat=2.
- **Stream returns 404**: Using wrong URL format. Use `/stream/{hash}/{id}` not `/stream?hash=X&id=Y`.
- **Torrent gets 0 peers**: Dead torrent. Filter by seed count in UI.
- **Port conflicts**: Don't run both Docker and binary on the same port.
