YouTube's only muxed mp4 is format 18 (360p), so the muxed-first selector was posting 360p even when 1080p was available. Split streams are now preferred for YouTube and merged locally; every other host keeps the muxed-first chain unchanged (verified identical selection for Instagram, X and Reddit). Three things the obvious version got wrong: - A height<=1080 cap excludes portrait video: a 1080p Short or reel is 1080x1920. Resolution is capped with -S res:1080 instead, which keys off the smaller dimension. - h264 is a hard first preference, not an -S sort key. At equal resolution YouTube's VP9/AV1 renditions are smaller and win on size, but neither reliably plays in an mp4 container on older Signal clients. Dropping a resolution rung to stay on h264 is the better trade, and is usually smaller too: a 10-min video takes 480p h264 (37 MB) over 720p AV1 (78 MB). - The audio cap matters. YouTube's 388k track is 29 MB by itself on a 10-min video, enough to push an otherwise-fine merge past the 100 MB Signal limit; abr<=160 takes the 129k track instead. The 88 MB video cap makes yt-dlp step down automatically on long videos, so a 3-min video lands at 1080p (80 MB) and a 10-min one at 480p. Clips get the same treatment — they're YouTube-only, and 60s at 1080p is only ~29 MB. /force is deliberately left muxed-first: it exists for the biggest, slowest videos, where 1080p would risk the rsync timeout. Measured, old -> new: 3-min 360p/11 MB -> 1080p/80 MB; 10-min 360p/27 MB -> 480p/37 MB; portrait Short 360x640/6 MB -> 1080x1920/23 MB. Verified end to end: real downloads decode as h264+aac, clip lands at exactly 60.000s.
80 lines
3.6 KiB
Markdown
80 lines
3.6 KiB
Markdown
# signal-bot
|
||
|
||
A Signal group-chat bot that grabs videos from links people post (X/Twitter,
|
||
Instagram, YouTube, TikTok, Reddit) and posts them back into the chat, plus a
|
||
couple of toys for messing with the last video.
|
||
|
||
## What it does
|
||
|
||
- **Auto-download** — watches every group message for a supported link and
|
||
replies with the video. Powered by [yt-dlp](https://github.com/yt-dlp/yt-dlp).
|
||
Videos over 100 MB are auto re-encoded with ffmpeg to fit. YouTube is fetched
|
||
as h264 up to 1080p, stepping down a resolution rung on longer videos so the
|
||
merge lands under the Signal size limit without a re-encode; other hosts take
|
||
their muxed rendition as-is.
|
||
- **`/speed [n]`** — re-posts the last video sped up `n`× (default 2×), pitch-shifted
|
||
like tape. e.g. `/speed 4`, `/speed 0.5`.
|
||
- **`/rev`** — re-posts the last video reversed (audio too).
|
||
- **`/cookies`** (DM, admins only) — paste exported `.instagram.com` cookie lines
|
||
to refresh Instagram auth without touching the server.
|
||
|
||
Supported links: `x.com` / `twitter.com` (and `fxtwitter`/`vxtwitter`/`fixupx`
|
||
wrappers), `instagram.com/reel|p`, `youtube.com` / `youtu.be` / Shorts, TikTok,
|
||
and Reddit (`/comments/` permalinks, `/s/` app share links, `redd.it` and
|
||
`v.redd.it` shortlinks). Reddit posts with no video stay silent.
|
||
|
||
## How it's wired
|
||
|
||
```
|
||
Signal ──► signal-cli-rest-api (json-rpc, :8080) ──► bot.py (signalbot lib)
|
||
[docker-compose.yml] [systemd service]
|
||
│
|
||
yt-dlp + ffmpeg + cookies.txt
|
||
```
|
||
|
||
- `bot.py` connects to a [signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api)
|
||
daemon over a websocket in `json-rpc` mode.
|
||
- `docker-compose.yml` runs that daemon; the linked-account data lives in a
|
||
`./signal-cli-data` volume (gitignored — it holds private keys).
|
||
- `signal-bot.service.example` is the systemd unit that runs `bot.py`.
|
||
|
||
## Setup
|
||
|
||
1. **Run signal-cli-rest-api and link it as a device** to your Signal account:
|
||
```
|
||
docker compose up -d
|
||
```
|
||
Then link via the QR at `http://127.0.0.1:8080/v1/qrcodelink?device_name=bot`
|
||
(scan from Signal → Settings → Linked devices). Keep signal-cli up to date —
|
||
an out-of-date signal-cli silently stops receiving messages when Signal
|
||
changes its server protocol (`getServerGuid must not be null` in the logs);
|
||
`docker compose pull && docker compose up -d` fixes it.
|
||
|
||
2. **Python env:**
|
||
```
|
||
python3 -m venv venv
|
||
venv/bin/pip install -r requirements.txt
|
||
```
|
||
YouTube also needs **Node >= 22** on PATH (for `yt-dlp-ejs`'s challenge
|
||
solver). If your distro only ships an older Node, install a standalone Node 22+
|
||
and point `bot.py`'s `_NODE22` at it. Note: YouTube cookies are deliberately
|
||
withheld (`_cookie_args`) — sending account cookies forces yt-dlp onto a player
|
||
client whose media URLs 403; cookieless YouTube requests work.
|
||
|
||
3. **Cookies:** copy `cookies.txt.example` to `cookies.txt` and fill in real
|
||
exported cookies for the sites you want auth'd. (gitignored)
|
||
|
||
4. **Service:** copy `signal-bot.service.example` to `signal-bot.service`, set
|
||
your user / phone number / paths, install it:
|
||
```
|
||
sudo cp signal-bot.service /etc/systemd/system/
|
||
sudo systemctl enable --now signal-bot
|
||
```
|
||
|
||
## Notes
|
||
|
||
- The bot only acts in **group** chats (except `/cookies`, which is DM-only).
|
||
- Reply-targeting (e.g. "speed up *this* video") was intentionally dropped:
|
||
Signal Desktop strips quote metadata from synced transcripts, so `/speed` and
|
||
`/rev` always act on the last video seen in the group.
|