anchorless 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anchorless-1.0.0/.dockerignore +8 -0
- anchorless-1.0.0/.gitignore +13 -0
- anchorless-1.0.0/CHANGELOG.md +5 -0
- anchorless-1.0.0/CLAUDE.md +46 -0
- anchorless-1.0.0/Dockerfile +29 -0
- anchorless-1.0.0/LICENSE +21 -0
- anchorless-1.0.0/PKG-INFO +136 -0
- anchorless-1.0.0/README.md +107 -0
- anchorless-1.0.0/SECURITY.md +47 -0
- anchorless-1.0.0/config.example.toml +175 -0
- anchorless-1.0.0/docs/guide.md +180 -0
- anchorless-1.0.0/local.example.toml +16 -0
- anchorless-1.0.0/notes.md +1 -0
- anchorless-1.0.0/pyproject.toml +65 -0
- anchorless-1.0.0/scripts/release.sh +34 -0
- anchorless-1.0.0/src/anchorless/__init__.py +8 -0
- anchorless-1.0.0/src/anchorless/breaking.py +61 -0
- anchorless-1.0.0/src/anchorless/cli.py +230 -0
- anchorless-1.0.0/src/anchorless/config.py +573 -0
- anchorless-1.0.0/src/anchorless/examples.py +33 -0
- anchorless-1.0.0/src/anchorless/fonts/Barlow-Bold.ttf +0 -0
- anchorless-1.0.0/src/anchorless/fonts/Barlow-Medium.ttf +0 -0
- anchorless-1.0.0/src/anchorless/fonts/Barlow-Regular.ttf +0 -0
- anchorless-1.0.0/src/anchorless/fonts/Barlow-SemiBold.ttf +0 -0
- anchorless-1.0.0/src/anchorless/fonts/BarlowCondensed-Bold.ttf +0 -0
- anchorless-1.0.0/src/anchorless/fonts/BarlowCondensed-ExtraBold.ttf +0 -0
- anchorless-1.0.0/src/anchorless/fonts/BarlowCondensed-SemiBold.ttf +0 -0
- anchorless-1.0.0/src/anchorless/fonts/OFL-Barlow.txt +93 -0
- anchorless-1.0.0/src/anchorless/fonts/OFL-pixelifysans.txt +93 -0
- anchorless-1.0.0/src/anchorless/fonts/OFL-pressstart2p.txt +93 -0
- anchorless-1.0.0/src/anchorless/fonts/OFL-silkscreen.txt +93 -0
- anchorless-1.0.0/src/anchorless/fonts/OFL-vt323.txt +93 -0
- anchorless-1.0.0/src/anchorless/fonts/PixelifySans.ttf +0 -0
- anchorless-1.0.0/src/anchorless/fonts/PressStart2P-Regular.ttf +0 -0
- anchorless-1.0.0/src/anchorless/fonts/Silkscreen-Bold.ttf +0 -0
- anchorless-1.0.0/src/anchorless/fonts/Silkscreen-Regular.ttf +0 -0
- anchorless-1.0.0/src/anchorless/fonts/VT323-Regular.ttf +0 -0
- anchorless-1.0.0/src/anchorless/headline.py +201 -0
- anchorless-1.0.0/src/anchorless/models.py +95 -0
- anchorless-1.0.0/src/anchorless/netguard.py +74 -0
- anchorless-1.0.0/src/anchorless/newsroom.py +175 -0
- anchorless-1.0.0/src/anchorless/passwords.py +52 -0
- anchorless-1.0.0/src/anchorless/pipeline.py +159 -0
- anchorless-1.0.0/src/anchorless/render/__init__.py +22 -0
- anchorless-1.0.0/src/anchorless/render/crawl.py +87 -0
- anchorless-1.0.0/src/anchorless/render/pages.py +364 -0
- anchorless-1.0.0/src/anchorless/render/stills.py +66 -0
- anchorless-1.0.0/src/anchorless/render/studio.py +754 -0
- anchorless-1.0.0/src/anchorless/render/theme.py +180 -0
- anchorless-1.0.0/src/anchorless/settings.py +629 -0
- anchorless-1.0.0/src/anchorless/sound.py +241 -0
- anchorless-1.0.0/src/anchorless/sources/__init__.py +0 -0
- anchorless-1.0.0/src/anchorless/sources/rss.py +129 -0
- anchorless-1.0.0/src/anchorless/store.py +99 -0
- anchorless-1.0.0/src/anchorless/stream/__init__.py +0 -0
- anchorless-1.0.0/src/anchorless/stream/admin.py +187 -0
- anchorless-1.0.0/src/anchorless/stream/encoder.py +252 -0
- anchorless-1.0.0/src/anchorless/stream/guide.py +99 -0
- anchorless-1.0.0/src/anchorless/stream/playout.py +238 -0
- anchorless-1.0.0/src/anchorless/stream/preview.py +49 -0
- anchorless-1.0.0/src/anchorless/stream/server.py +556 -0
- anchorless-1.0.0/src/anchorless/stream/tsbuffer.py +148 -0
- anchorless-1.0.0/src/anchorless/stream/web.py +274 -0
- anchorless-1.0.0/src/anchorless/text.py +107 -0
- anchorless-1.0.0/src/anchorless/web/assets/settings.css +92 -0
- anchorless-1.0.0/src/anchorless/web/assets/settings.js +501 -0
- anchorless-1.0.0/src/anchorless/web/assets/style.css +179 -0
- anchorless-1.0.0/src/anchorless/web/assets/viewer.js +305 -0
- anchorless-1.0.0/src/anchorless/web/index.html +73 -0
- anchorless-1.0.0/src/anchorless/web/settings.html +46 -0
- anchorless-1.0.0/src/anchorless/web/vendor/mpegts.LICENSE +202 -0
- anchorless-1.0.0/src/anchorless/web/vendor/mpegts.min.js +9 -0
- anchorless-1.0.0/tests/__init__.py +0 -0
- anchorless-1.0.0/tests/conftest.py +30 -0
- anchorless-1.0.0/tests/fixtures/outlet_a.xml +18 -0
- anchorless-1.0.0/tests/fixtures/outlet_b.xml +15 -0
- anchorless-1.0.0/tests/test_config.py +117 -0
- anchorless-1.0.0/tests/test_headline.py +143 -0
- anchorless-1.0.0/tests/test_pipeline.py +79 -0
- anchorless-1.0.0/tests/test_settings.py +490 -0
- anchorless-1.0.0/tests/test_stream.py +345 -0
- anchorless-1.0.0/tests/test_text.py +43 -0
- anchorless-1.0.0/uv.lock +1076 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 — 2026-11-02
|
|
4
|
+
|
|
5
|
+
First release: a 24/7 text newscast for Jellyfin, Plex, Channels DVR or any IPTV player. Headlines come in from public RSS feeds (25 built in, from 11 outlets, plus any you add), the same story is grouped across outlets, and a rundown of top stories, local, national, world and business news plays as an MPEG-TS stream with an M3U playlist and XMLTV guide. Breaking news interrupts the rundown when many outlets pick a story up at once (local stories too, with a higher bar). Two looks: the pixel-art **Studio**, an empty news set with a day-night sky, and **Classic** cable-news pages (light, dark, or switching with the time of day). A browser viewer (with HLS for iPhones) and status page, and a password-protected settings page that edits the config file in place. Built to be self-hosted safely: feed fetches can't reach private networks, viewer limits, security headers, a hashed settings password, https-only settings logins and a change history. Frames are drawn in Python and encoded by ffmpeg, with VA-API hardware encoding, and nothing runs while nobody is watching. Install with `pip install anchorless` and `anchorless init`, or as a container image for amd64 and arm64.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
Anchorless: a 24/7 text newscast for IPTV. Public RSS headlines in, an MPEG-TS stream (plus M3U playlist and XMLTV guide) out, for Jellyfin, Plex, Channels DVR or any IPTV player. No browser: Pillow draws every frame, ffmpeg encodes. Python 3.12+, packaged with hatchling, run with uv. Shipped on PyPI (`pip install anchorless`, then `anchorless init`, needs ffmpeg) and as a container image.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv sync # create/refresh .venv from uv.lock
|
|
13
|
+
uv run anchorless init # writes config.toml (gitignored) from config.example.toml
|
|
14
|
+
uv run anchorless serve # the station on :9798 (viewer at /, settings at /settings)
|
|
15
|
+
uv run anchorless check-config # validate the config (never prints secrets)
|
|
16
|
+
uv run anchorless headlines # every multi-outlet story's versions, scored
|
|
17
|
+
uv run pytest # tests
|
|
18
|
+
uv run ruff check . # lint (CI runs both)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Visual changes (anything in `render/`) can't be judged from tests: render frames and look at them.
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
- `sources/rss.py` fetches feeds (httpx, through the SSRF guard in `netguard.py`) -> `Story`s (`models.py`).
|
|
26
|
+
- `pipeline.py` groups the same story across outlets into `Cluster`s (headline word overlap, then TF-IDF) and builds the rundown; `cluster.stories[0]` is the lead, i.e. the headline shown.
|
|
27
|
+
- `headline.py` is the "consensus headline": which outlet's wording to show. It must stay **outlet-blind** (no outlet is named or rated anywhere) and conservative: it only replaces a headline with a red flag. `[rundown] headline_choice = "log"` runs it in shadow mode.
|
|
28
|
+
- `breaking.py` decides breaking news; `newsroom.py` refreshes on a timer and holds the current state.
|
|
29
|
+
- `render/`: `studio.py` (the pixel-art set, an isometric 2:1 projection: centre things in 3D, a box top's visual centre is half its depth back from the front face), `pages.py` (Classic), `crawl.py`, `theme.py`.
|
|
30
|
+
- `stream/`: `server.py` (`Station` and `Channel`s: ffmpeg starts on the first viewer and stops after the last; viewer limits), `encoder.py`, `playout.py`, `tsbuffer.py` (HLS cut from memory), `web.py` (viewer, status, settings API, security headers/CSP), `admin.py` (settings login).
|
|
31
|
+
- `config.py`: pydantic models; env overrides `ANCHORLESS__SECTION__KEY`. `settings.py`: the settings page edits the TOML file in place with tomlkit. **The file is the source of truth**: per-field conflict checks, validate before writing, atomic replace, a change history.
|
|
32
|
+
|
|
33
|
+
## Rules
|
|
34
|
+
|
|
35
|
+
- **No personal or local details in the repo.** Locations and local outlet names live only in a user's `.env` / local feeds file. Test fixtures, examples, screenshots and the README GIF use made-up outlets and headlines.
|
|
36
|
+
- Security behaviour is documented in SECURITY.md and the guide's "Hosting publicly" checklist: keep them true when changing `netguard.py`, `admin.py`, `web.py` or the `[stream]` limits.
|
|
37
|
+
- No inline script or style in the web pages (the CSP forbids it).
|
|
38
|
+
|
|
39
|
+
## Releasing
|
|
40
|
+
|
|
41
|
+
1. Bump `version` in `pyproject.toml` (the only place it lives; `anchorless.__version__` reads it), run `uv lock`.
|
|
42
|
+
2. Add `## X.Y.Z — YYYY-MM-DD` at the top of `CHANGELOG.md`: one paragraph, what changed for users.
|
|
43
|
+
3. Commit and push to main, then `scripts/release.sh`: it checks everything and pushes the tag `vX.Y.Z`.
|
|
44
|
+
4. `.github/workflows/release.yml` tests again, then publishes to PyPI (trusted publishing, the `pypi` environment), pushes `ghcr.io/gabebecker2048/anchorless` (`X.Y.Z`, `X.Y`, `X`, `latest`; amd64 + arm64), and creates the GitHub release from the CHANGELOG entry with the wheel and sdist attached.
|
|
45
|
+
|
|
46
|
+
`config.example.toml` and `local.example.toml` are what `anchorless init` writes: the wheel carries them (pyproject force-include), and `examples.py` falls back to the repo root in a checkout. The README embeds `deploy/docker-compose.example.yml` verbatim (a test checks it).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Anchorless: a 24/7 text newscast for IPTV.
|
|
2
|
+
FROM python:3.12-slim
|
|
3
|
+
|
|
4
|
+
# ffmpeg, plus VA-API drivers for hardware encoding (Intel: iHD, AMD: Mesa).
|
|
5
|
+
# The Intel driver lives in Debian's non-free component, and only exists for amd64.
|
|
6
|
+
RUN sed -i 's/^Components: main$/Components: main non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources \
|
|
7
|
+
&& apt-get update \
|
|
8
|
+
&& apt-get install -y --no-install-recommends ffmpeg mesa-va-drivers vainfo \
|
|
9
|
+
$([ "$(dpkg --print-architecture)" = amd64 ] && echo intel-media-va-driver-non-free) \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11
|
+
|
|
12
|
+
COPY --from=ghcr.io/astral-sh/uv:0.12.4 /uv /usr/local/bin/uv
|
|
13
|
+
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never
|
|
16
|
+
COPY pyproject.toml uv.lock README.md LICENSE config.example.toml local.example.toml ./
|
|
17
|
+
RUN uv sync --frozen --no-dev --no-install-project
|
|
18
|
+
COPY src ./src
|
|
19
|
+
RUN uv sync --frozen --no-dev
|
|
20
|
+
|
|
21
|
+
RUN useradd --system --uid 10001 --home /data anchorless \
|
|
22
|
+
&& mkdir -p /data /config && chown anchorless /data /config
|
|
23
|
+
USER anchorless
|
|
24
|
+
ENV PATH="/app/.venv/bin:$PATH" ANCHORLESS_CONFIG=/config/config.toml ANCHORLESS__DATA__DIR=/data PYTHONUNBUFFERED=1
|
|
25
|
+
VOLUME /data /config
|
|
26
|
+
EXPOSE 9798
|
|
27
|
+
HEALTHCHECK --interval=60s --timeout=5s --start-period=30s \
|
|
28
|
+
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9798/health', timeout=4)"
|
|
29
|
+
CMD ["anchorless", "serve", "--init"]
|
anchorless-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabe Becker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: anchorless
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A 24/7 text newscast for IPTV: headlines in, MPEG-TS out. News without the anchor.
|
|
5
|
+
Project-URL: Homepage, https://github.com/GabeBecker2048/anchorless
|
|
6
|
+
Project-URL: Changelog, https://github.com/GabeBecker2048/anchorless/blob/main/CHANGELOG.md
|
|
7
|
+
Project-URL: Issues, https://github.com/GabeBecker2048/anchorless/issues
|
|
8
|
+
Author: Gabe Becker
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: iptv,jellyfin,news,pixel-art,plex,rss,self-hosted,tv
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Multimedia :: Video
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: aiohttp>=3.10
|
|
23
|
+
Requires-Dist: feedparser>=6.0
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: pillow>=11.0
|
|
26
|
+
Requires-Dist: pydantic>=2.8
|
|
27
|
+
Requires-Dist: tomlkit>=0.15.2
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Anchorless
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/anchorless/)
|
|
33
|
+
[](https://pypi.org/project/anchorless/)
|
|
34
|
+
[](https://github.com/GabeBecker2048/anchorless/pkgs/container/anchorless)
|
|
35
|
+
[](https://github.com/GabeBecker2048/anchorless/blob/main/LICENSE)
|
|
36
|
+
[](https://github.com/GabeBecker2048/anchorless/actions/workflows/test.yml)
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
<sub>Recorded from the real playout, sped up. The headlines and outlets are made up.</sub>
|
|
41
|
+
|
|
42
|
+
**News without the anchor.** Anchorless turns news headlines into a 24/7 TV
|
|
43
|
+
channel you can add to Jellyfin, Plex, Channels DVR or any IPTV player: an empty
|
|
44
|
+
pixel-art news set where the news plays on the big screen behind a desk nobody
|
|
45
|
+
sits at - with a headline crawl, background music and breaking-news alerts.
|
|
46
|
+
|
|
47
|
+
- Pulls headlines from public RSS feeds, groups the same story across outlets,
|
|
48
|
+
and builds a rundown (top stories, local, national, world, business).
|
|
49
|
+
- Breaking news interrupts the rundown when many outlets pick a story up at once.
|
|
50
|
+
- Two looks: the pixel-art **Studio**, or **Classic** cable-news pages (light, dark,
|
|
51
|
+
or switching with the time of day).
|
|
52
|
+
- Lightweight: no browser - Python draws the frames, ffmpeg encodes them, and
|
|
53
|
+
nothing runs while nobody is watching.
|
|
54
|
+
- Watch it in a browser too, and change its settings there (optional, password
|
|
55
|
+
protected).
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
Anchorless runs as a Docker container, or natively with Python 3.12+ and
|
|
60
|
+
[ffmpeg](https://ffmpeg.org/download.html).
|
|
61
|
+
|
|
62
|
+
### Docker
|
|
63
|
+
|
|
64
|
+
Save this as `docker-compose.yml` in a new folder:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
services:
|
|
68
|
+
anchorless:
|
|
69
|
+
image: ghcr.io/gabebecker2048/anchorless:latest # or :1 to stay on 1.x
|
|
70
|
+
restart: unless-stopped
|
|
71
|
+
user: "1000:1000" # who owns ./config and ./data (`id -u`:`id -g`)
|
|
72
|
+
ports:
|
|
73
|
+
- "9798:9798"
|
|
74
|
+
volumes:
|
|
75
|
+
- ./config:/config # config.toml: written on first start, edit it any time
|
|
76
|
+
- ./data:/data
|
|
77
|
+
# - /path/to/music:/music:ro # a folder of MP3s to play underneath
|
|
78
|
+
# environment:
|
|
79
|
+
# ANCHORLESS__LOCATION__CITY: Springfield # local news (see config.toml)
|
|
80
|
+
# ANCHORLESS__ADMIN__PASSWORD: <long random> # turns on the settings page
|
|
81
|
+
# Hardware encoding (Intel/AMD): uncomment, set the host's render group id
|
|
82
|
+
# (getent group render), and set encoder = "vaapi" in config.toml.
|
|
83
|
+
# devices: ["/dev/dri:/dev/dri"]
|
|
84
|
+
# group_add: ["992"]
|
|
85
|
+
# Hardening: Anchorless only writes to /config, /data and /tmp.
|
|
86
|
+
read_only: true
|
|
87
|
+
tmpfs: [/tmp]
|
|
88
|
+
cap_drop: [ALL]
|
|
89
|
+
security_opt: ["no-new-privileges:true"]
|
|
90
|
+
mem_limit: 512m
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Then, in that folder:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
mkdir config data # so they're yours, not root's
|
|
97
|
+
docker compose up -d
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The first start writes `config/config.toml`, with every setting explained.
|
|
101
|
+
Update with `docker compose pull && docker compose up -d`.
|
|
102
|
+
|
|
103
|
+
### Native
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
pip install anchorless # or: uv tool install anchorless
|
|
107
|
+
anchorless init # writes config.toml here
|
|
108
|
+
anchorless serve
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
ffmpeg must be on the PATH (`apt install ffmpeg`, `brew install ffmpeg`, ...).
|
|
112
|
+
Run `anchorless serve` from the folder with `config.toml`, or point at it with
|
|
113
|
+
`anchorless -c /path/to/config.toml serve`.
|
|
114
|
+
|
|
115
|
+
### Watch
|
|
116
|
+
|
|
117
|
+
Open `http://<host>:9798/` to watch in a browser, or add it to Jellyfin, Plex or
|
|
118
|
+
any IPTV player as an M3U tuner (`http://<host>:9798/playlist.m3u`) with an
|
|
119
|
+
XMLTV guide (`http://<host>:9798/guide.xml`).
|
|
120
|
+
|
|
121
|
+
## More
|
|
122
|
+
|
|
123
|
+
- [The guide](https://github.com/GabeBecker2048/anchorless/blob/main/docs/guide.md): Jellyfin setup, themes and channels, the settings
|
|
124
|
+
page, hosting publicly, and configuration
|
|
125
|
+
- [`config.example.toml`](https://github.com/GabeBecker2048/anchorless/blob/main/config.example.toml): every setting, explained (what `anchorless init` writes)
|
|
126
|
+
- [SECURITY.md](https://github.com/GabeBecker2048/anchorless/blob/main/SECURITY.md): reporting a vulnerability
|
|
127
|
+
- [CHANGELOG.md](https://github.com/GabeBecker2048/anchorless/blob/main/CHANGELOG.md): what changed in each release
|
|
128
|
+
|
|
129
|
+
Anchorless doesn't include music: point it at a folder of MP3s you have the
|
|
130
|
+
right to play (`[audio] music_dir`), or it runs with just its own sound effects.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT. The bundled fonts are under the SIL Open Font License and the bundled
|
|
135
|
+
[mpegts.js](https://github.com/xqq/mpegts.js) player under Apache 2.0. Headlines
|
|
136
|
+
belong to their outlets; see [Sources and fair use](https://github.com/GabeBecker2048/anchorless/blob/main/docs/guide.md#sources-and-fair-use).
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Anchorless
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/anchorless/)
|
|
4
|
+
[](https://pypi.org/project/anchorless/)
|
|
5
|
+
[](https://github.com/GabeBecker2048/anchorless/pkgs/container/anchorless)
|
|
6
|
+
[](https://github.com/GabeBecker2048/anchorless/blob/main/LICENSE)
|
|
7
|
+
[](https://github.com/GabeBecker2048/anchorless/actions/workflows/test.yml)
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
<sub>Recorded from the real playout, sped up. The headlines and outlets are made up.</sub>
|
|
12
|
+
|
|
13
|
+
**News without the anchor.** Anchorless turns news headlines into a 24/7 TV
|
|
14
|
+
channel you can add to Jellyfin, Plex, Channels DVR or any IPTV player: an empty
|
|
15
|
+
pixel-art news set where the news plays on the big screen behind a desk nobody
|
|
16
|
+
sits at - with a headline crawl, background music and breaking-news alerts.
|
|
17
|
+
|
|
18
|
+
- Pulls headlines from public RSS feeds, groups the same story across outlets,
|
|
19
|
+
and builds a rundown (top stories, local, national, world, business).
|
|
20
|
+
- Breaking news interrupts the rundown when many outlets pick a story up at once.
|
|
21
|
+
- Two looks: the pixel-art **Studio**, or **Classic** cable-news pages (light, dark,
|
|
22
|
+
or switching with the time of day).
|
|
23
|
+
- Lightweight: no browser - Python draws the frames, ffmpeg encodes them, and
|
|
24
|
+
nothing runs while nobody is watching.
|
|
25
|
+
- Watch it in a browser too, and change its settings there (optional, password
|
|
26
|
+
protected).
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
Anchorless runs as a Docker container, or natively with Python 3.12+ and
|
|
31
|
+
[ffmpeg](https://ffmpeg.org/download.html).
|
|
32
|
+
|
|
33
|
+
### Docker
|
|
34
|
+
|
|
35
|
+
Save this as `docker-compose.yml` in a new folder:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
services:
|
|
39
|
+
anchorless:
|
|
40
|
+
image: ghcr.io/gabebecker2048/anchorless:latest # or :1 to stay on 1.x
|
|
41
|
+
restart: unless-stopped
|
|
42
|
+
user: "1000:1000" # who owns ./config and ./data (`id -u`:`id -g`)
|
|
43
|
+
ports:
|
|
44
|
+
- "9798:9798"
|
|
45
|
+
volumes:
|
|
46
|
+
- ./config:/config # config.toml: written on first start, edit it any time
|
|
47
|
+
- ./data:/data
|
|
48
|
+
# - /path/to/music:/music:ro # a folder of MP3s to play underneath
|
|
49
|
+
# environment:
|
|
50
|
+
# ANCHORLESS__LOCATION__CITY: Springfield # local news (see config.toml)
|
|
51
|
+
# ANCHORLESS__ADMIN__PASSWORD: <long random> # turns on the settings page
|
|
52
|
+
# Hardware encoding (Intel/AMD): uncomment, set the host's render group id
|
|
53
|
+
# (getent group render), and set encoder = "vaapi" in config.toml.
|
|
54
|
+
# devices: ["/dev/dri:/dev/dri"]
|
|
55
|
+
# group_add: ["992"]
|
|
56
|
+
# Hardening: Anchorless only writes to /config, /data and /tmp.
|
|
57
|
+
read_only: true
|
|
58
|
+
tmpfs: [/tmp]
|
|
59
|
+
cap_drop: [ALL]
|
|
60
|
+
security_opt: ["no-new-privileges:true"]
|
|
61
|
+
mem_limit: 512m
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then, in that folder:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
mkdir config data # so they're yours, not root's
|
|
68
|
+
docker compose up -d
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The first start writes `config/config.toml`, with every setting explained.
|
|
72
|
+
Update with `docker compose pull && docker compose up -d`.
|
|
73
|
+
|
|
74
|
+
### Native
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
pip install anchorless # or: uv tool install anchorless
|
|
78
|
+
anchorless init # writes config.toml here
|
|
79
|
+
anchorless serve
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
ffmpeg must be on the PATH (`apt install ffmpeg`, `brew install ffmpeg`, ...).
|
|
83
|
+
Run `anchorless serve` from the folder with `config.toml`, or point at it with
|
|
84
|
+
`anchorless -c /path/to/config.toml serve`.
|
|
85
|
+
|
|
86
|
+
### Watch
|
|
87
|
+
|
|
88
|
+
Open `http://<host>:9798/` to watch in a browser, or add it to Jellyfin, Plex or
|
|
89
|
+
any IPTV player as an M3U tuner (`http://<host>:9798/playlist.m3u`) with an
|
|
90
|
+
XMLTV guide (`http://<host>:9798/guide.xml`).
|
|
91
|
+
|
|
92
|
+
## More
|
|
93
|
+
|
|
94
|
+
- [The guide](https://github.com/GabeBecker2048/anchorless/blob/main/docs/guide.md): Jellyfin setup, themes and channels, the settings
|
|
95
|
+
page, hosting publicly, and configuration
|
|
96
|
+
- [`config.example.toml`](https://github.com/GabeBecker2048/anchorless/blob/main/config.example.toml): every setting, explained (what `anchorless init` writes)
|
|
97
|
+
- [SECURITY.md](https://github.com/GabeBecker2048/anchorless/blob/main/SECURITY.md): reporting a vulnerability
|
|
98
|
+
- [CHANGELOG.md](https://github.com/GabeBecker2048/anchorless/blob/main/CHANGELOG.md): what changed in each release
|
|
99
|
+
|
|
100
|
+
Anchorless doesn't include music: point it at a folder of MP3s you have the
|
|
101
|
+
right to play (`[audio] music_dir`), or it runs with just its own sound effects.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT. The bundled fonts are under the SIL Open Font License and the bundled
|
|
106
|
+
[mpegts.js](https://github.com/xqq/mpegts.js) player under Apache 2.0. Headlines
|
|
107
|
+
belong to their outlets; see [Sources and fair use](https://github.com/GabeBecker2048/anchorless/blob/main/docs/guide.md#sources-and-fair-use).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report security problems **privately**, not in a public issue:
|
|
6
|
+
use GitHub's **"Report a vulnerability"** button on this repository's
|
|
7
|
+
**Security** tab. Only the maintainer can see the report.
|
|
8
|
+
|
|
9
|
+
Please include what you found, how to reproduce it, and which version (or
|
|
10
|
+
commit) you used. Anchorless is a hobby project, so there is no guaranteed
|
|
11
|
+
response time, but reports are taken seriously: you'll get an answer, a fix
|
|
12
|
+
where one is needed, and credit in the advisory if you'd like it.
|
|
13
|
+
|
|
14
|
+
## Supported versions
|
|
15
|
+
|
|
16
|
+
Only the latest release (and `main`) gets security fixes.
|
|
17
|
+
|
|
18
|
+
| Version | Supported |
|
|
19
|
+
|---|---|
|
|
20
|
+
| 1.x | yes |
|
|
21
|
+
|
|
22
|
+
## What counts
|
|
23
|
+
|
|
24
|
+
**In scope** - for example:
|
|
25
|
+
|
|
26
|
+
- getting into the settings page, or changing settings, without the password
|
|
27
|
+
- getting past `[admin] allow_from`, `trusted_proxies` or `require_https`
|
|
28
|
+
- making the server fetch a private-network address despite
|
|
29
|
+
`[feeds] allow_private = false`
|
|
30
|
+
- reading files or secrets: the password, the config, the local feeds file
|
|
31
|
+
- script injection in the viewer or settings page (headlines are untrusted)
|
|
32
|
+
- a stream or request that crashes the server or exhausts its memory despite the
|
|
33
|
+
`[stream]` limits
|
|
34
|
+
|
|
35
|
+
**Not a vulnerability** - by design (see "Hosting publicly" in the README):
|
|
36
|
+
|
|
37
|
+
- anyone who can reach the TV side (`/playlist.m3u`, `/guide.xml`, `/live.ts`,
|
|
38
|
+
`/hls/`) or the viewer page can watch, and watching costs CPU - that is what
|
|
39
|
+
`max_sessions`, `max_viewers` and `max_viewers_per_client` are for
|
|
40
|
+
- problems that need the settings password, or access to the server or its
|
|
41
|
+
config files
|
|
42
|
+
- weaker setups chosen on purpose, such as `require_https = false` or a wide
|
|
43
|
+
`allow_from`
|
|
44
|
+
|
|
45
|
+
## Security advisories
|
|
46
|
+
|
|
47
|
+
None yet.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Anchorless configuration.
|
|
2
|
+
#
|
|
3
|
+
# Copy this to config.toml and change what you like. In [channel], theme and
|
|
4
|
+
# show_on are required (and channel_number for a TV channel); every other
|
|
5
|
+
# setting is optional, and anything left out uses the default shown here.
|
|
6
|
+
#
|
|
7
|
+
# Any setting can also come from the environment as ANCHORLESS__<SECTION>__<KEY>,
|
|
8
|
+
# e.g. ANCHORLESS__LOCATION__CITY=Springfield. Use that for anything personal
|
|
9
|
+
# (like your location) so this file can be shared.
|
|
10
|
+
|
|
11
|
+
[channel]
|
|
12
|
+
theme = "studio" # required: "studio" (pixel-art news set), "classic-light" / "classic-dark"
|
|
13
|
+
# (cable-news pages), or "classic" (light by day, dark at night)
|
|
14
|
+
name = "NEWS" # shown in the logo and in your TV guide (default "NEWS")
|
|
15
|
+
show_on = "both" # required: "both" (TV + web page), "tv" (TV only) or "web" (web page only)
|
|
16
|
+
channel_number = 101 # required for "both" and "tv" (e.g. 2, 101, 2.1); ignored for "web"
|
|
17
|
+
# id = "anchorless.news" # stable id for TV guides (tvg-id) and the stream URL
|
|
18
|
+
timezone = "America/Chicago" # for the on-screen clock and the guide
|
|
19
|
+
guide_title = "Anchorless News"
|
|
20
|
+
guide_description = "Around-the-clock headlines from national and world news outlets."
|
|
21
|
+
guide_slot_minutes = 30 # guide blocks on :00/:30; 1440 = one per day
|
|
22
|
+
|
|
23
|
+
[location]
|
|
24
|
+
# Local news. Best set from the environment instead (ANCHORLESS__LOCATION__CITY=...),
|
|
25
|
+
# so this file names no place:
|
|
26
|
+
# city = "Springfield" # the local segment's title
|
|
27
|
+
# sources_file = "local.toml" # your local outlets' feeds (see local.example.toml),
|
|
28
|
+
# # relative to this file. No file = no local segment.
|
|
29
|
+
|
|
30
|
+
[video]
|
|
31
|
+
width = 854
|
|
32
|
+
height = 480
|
|
33
|
+
fps = 24
|
|
34
|
+
encoder = "x264" # "x264" (CPU) or "vaapi" (Intel/AMD GPU)
|
|
35
|
+
vaapi_device = "/dev/dri/renderD128"
|
|
36
|
+
vaapi_rc = "vbr" # "cqp" for older Intel GPUs (i965 driver) that reject bitrates
|
|
37
|
+
vaapi_qp = 24 # quality for "cqp" (lower = better)
|
|
38
|
+
vaapi_convert = "cpu" # "gpu" converts colours on the GPU too (less CPU; newer Intel drivers)
|
|
39
|
+
bitrate = "1500k"
|
|
40
|
+
maxrate = "2000k"
|
|
41
|
+
gop_seconds = 2 # a keyframe every 2 s: how fast a joining viewer can start
|
|
42
|
+
x264_preset = "veryfast"
|
|
43
|
+
x264_threads = 0 # 0 = automatic
|
|
44
|
+
|
|
45
|
+
[audio]
|
|
46
|
+
music_dir = "/music" # background music (.mp3), shuffled; missing folder = silence
|
|
47
|
+
volume = 0.5
|
|
48
|
+
bitrate = "128k"
|
|
49
|
+
|
|
50
|
+
[stream]
|
|
51
|
+
host = "0.0.0.0"
|
|
52
|
+
port = 9798
|
|
53
|
+
lead_seconds = 10 # rendered ahead for a new viewer (Samsung TVs buffer ~10 s)
|
|
54
|
+
idle_seconds = 60 # stop encoding this long after the last viewer leaves
|
|
55
|
+
max_sessions = 2 # channels encoding at once (~30% of a CPU core each); -1 = no limit,
|
|
56
|
+
# 0 = listed but nobody can watch. Viewers of a channel already on don't count.
|
|
57
|
+
max_viewers = -1 # viewers at once, all channels (TV, web, iPhone); -1 = no limit, 0 = nobody
|
|
58
|
+
max_viewers_per_client = -1 # viewers from one address; -1 = no limit. Jellyfin (all its viewers
|
|
59
|
+
# come from it) and a household behind one router are one address each.
|
|
60
|
+
|
|
61
|
+
[web] # the viewer page; the TV side (playlist, guide, streams) is never https-only
|
|
62
|
+
require_https = false # true: viewer page only over https. Suggested if you have https - and
|
|
63
|
+
# keep [admin] require_https on too.
|
|
64
|
+
public_status = "full" # "full": /health and the viewer page list feeds by name and site;
|
|
65
|
+
# "summary": counts only. URLs and error details: settings page only.
|
|
66
|
+
|
|
67
|
+
[rundown]
|
|
68
|
+
refresh_minutes = 10 # how often the sources are fetched
|
|
69
|
+
page_seconds = 12 # how long each story stays on screen
|
|
70
|
+
intro_seconds = 5 # segment title card (0 = none)
|
|
71
|
+
transition_seconds = 0.4 # crossfade between pages (0 = cut)
|
|
72
|
+
max_story_age_hours = 12
|
|
73
|
+
same_story_similarity = 0.5 # headline overlap that makes two items the same story
|
|
74
|
+
same_story_text_similarity = 0.32
|
|
75
|
+
|
|
76
|
+
# The newscast: segments play in order, then it loops. A story appears in the
|
|
77
|
+
# first segment it fits and nowhere else. Leave out scope/category to take any story.
|
|
78
|
+
# When several outlets carry a story, whose headline is shown: "first" (first
|
|
79
|
+
# published), "consensus" (if that one has a red flag - loaded words, shouting, or
|
|
80
|
+
# wording no other outlet shares - a clean version the others agree with), or "log"
|
|
81
|
+
# (show "first"; list what consensus would change, on the settings page and in the log).
|
|
82
|
+
headline_choice = "log"
|
|
83
|
+
headline_margin = 0.15 # how much better a version must score to replace the shown one
|
|
84
|
+
|
|
85
|
+
[[rundown.segment]]
|
|
86
|
+
name = "TOP STORIES"
|
|
87
|
+
stories = 4
|
|
88
|
+
exclude_scope = ["local"] # local-only stories stay in the local segment
|
|
89
|
+
|
|
90
|
+
[[rundown.segment]]
|
|
91
|
+
name = "{city}" # the local segment, titled with [location] city
|
|
92
|
+
stories = 3
|
|
93
|
+
scope = "local"
|
|
94
|
+
|
|
95
|
+
[[rundown.segment]]
|
|
96
|
+
name = "NATIONAL"
|
|
97
|
+
stories = 3
|
|
98
|
+
scope = "national" # local | national | world
|
|
99
|
+
|
|
100
|
+
[[rundown.segment]]
|
|
101
|
+
name = "WORLD"
|
|
102
|
+
stories = 3
|
|
103
|
+
scope = "world"
|
|
104
|
+
|
|
105
|
+
[[rundown.segment]]
|
|
106
|
+
name = "BUSINESS"
|
|
107
|
+
stories = 2
|
|
108
|
+
category = "business"
|
|
109
|
+
|
|
110
|
+
[breaking]
|
|
111
|
+
enabled = true
|
|
112
|
+
min_sources = 4 # outlets that must carry a story...
|
|
113
|
+
window_minutes = 30 # ...within this long of its first appearance
|
|
114
|
+
cooldown_minutes = 20 # at most one alert per this long
|
|
115
|
+
crawl_tag_minutes = 60 # how long it keeps the red BREAKING tag in the crawl
|
|
116
|
+
title_prefixes = ["BREAKING:", "BREAKING NEWS:"]
|
|
117
|
+
local_min_sources = 5 # a story only local outlets carry needs this many
|
|
118
|
+
local_alerts = "on" # "on", "log" (only log what would break in), or "off"
|
|
119
|
+
sound = "auto" # "auto", "chiptune", "news", "none", or a sound file's path
|
|
120
|
+
sound_volume = 0.8 # the music ducks under it either way
|
|
121
|
+
|
|
122
|
+
[crawl] # the headline crawl along the bottom, on every channel
|
|
123
|
+
label = "HEADLINES"
|
|
124
|
+
speed = 96 # pixels per second at 480 lines
|
|
125
|
+
|
|
126
|
+
# More channels from the same news (one fetch feeds them all). The settings are
|
|
127
|
+
# the same as [channel], except that name is required here. Each channel gets its
|
|
128
|
+
# own stream, drawn and encoded only while someone watches it. Add as many as you like.
|
|
129
|
+
#
|
|
130
|
+
# [[extra_channel]]
|
|
131
|
+
# theme = "classic"
|
|
132
|
+
# name = "NEWS CLASSIC" # required for an extra channel
|
|
133
|
+
# show_on = "web"
|
|
134
|
+
# channel_number = 99 # ignored: show_on is "web"
|
|
135
|
+
#
|
|
136
|
+
# [[extra_channel]]
|
|
137
|
+
# theme = "studio"
|
|
138
|
+
# name = "NEWS 2"
|
|
139
|
+
# show_on = "tv"
|
|
140
|
+
# channel_number = 102
|
|
141
|
+
|
|
142
|
+
[data]
|
|
143
|
+
dir = "data" # SQLite memory of stories already seen
|
|
144
|
+
|
|
145
|
+
# Feeds. Anchorless comes with a list of national and world outlets (NPR, NYT,
|
|
146
|
+
# CBS, ABC, NBC, Fox, Guardian, BBC, Al Jazeera, CNBC). [[source]] entries are
|
|
147
|
+
# added to that list; turn any feed off by its URL. Feeds with the same name
|
|
148
|
+
# count as one outlet. scope: national | world (local feeds go in the local
|
|
149
|
+
# feeds file, see [location]). exclude: regular expressions; matching
|
|
150
|
+
# headlines or links are skipped.
|
|
151
|
+
[feeds]
|
|
152
|
+
builtin = true # false: only your [[source]] entries
|
|
153
|
+
disabled = [] # feed URLs to skip, e.g. ["https://feeds.npr.org/1001/rss.xml"]
|
|
154
|
+
allow_private = false # true: allow feeds on your own network. Off, every feed request
|
|
155
|
+
# (redirects too) must go to a public address - so a URL typed
|
|
156
|
+
# into the settings page can't point the server at your router etc.
|
|
157
|
+
|
|
158
|
+
# [[source]]
|
|
159
|
+
# name = "Reuters"
|
|
160
|
+
# url = "https://example.com/reuters/rss.xml"
|
|
161
|
+
# scope = "world"
|
|
162
|
+
# category = "general" # "business" feeds the BUSINESS segment
|
|
163
|
+
|
|
164
|
+
# The settings page (/settings) edits this file. It is off until a password is
|
|
165
|
+
# set (12+ characters): either from the environment, ANCHORLESS__ADMIN__PASSWORD=...,
|
|
166
|
+
# or as a hash here - run `anchorless hash-password` and paste its line:
|
|
167
|
+
# password_hash = "scrypt$15$8$1$...$..."
|
|
168
|
+
# The file must be writable by Anchorless (in Docker: mount its folder, not the file).
|
|
169
|
+
[admin]
|
|
170
|
+
allow_from = ["127.0.0.0/8", "::1/128", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
|
171
|
+
"100.64.0.0/10", "fc00::/7", "fe80::/10"] # who may open the settings page
|
|
172
|
+
trusted_proxies = [] # e.g. ["192.168.1.2"]: your reverse proxy, so X-Forwarded-For is believed
|
|
173
|
+
session_days = 30 # how long a login lasts
|
|
174
|
+
require_https = true # log in only over https (TLS, or a trusted proxy's X-Forwarded-Proto);
|
|
175
|
+
# localhost is always allowed. false only on a network you trust.
|