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.
Files changed (83) hide show
  1. anchorless-1.0.0/.dockerignore +8 -0
  2. anchorless-1.0.0/.gitignore +13 -0
  3. anchorless-1.0.0/CHANGELOG.md +5 -0
  4. anchorless-1.0.0/CLAUDE.md +46 -0
  5. anchorless-1.0.0/Dockerfile +29 -0
  6. anchorless-1.0.0/LICENSE +21 -0
  7. anchorless-1.0.0/PKG-INFO +136 -0
  8. anchorless-1.0.0/README.md +107 -0
  9. anchorless-1.0.0/SECURITY.md +47 -0
  10. anchorless-1.0.0/config.example.toml +175 -0
  11. anchorless-1.0.0/docs/guide.md +180 -0
  12. anchorless-1.0.0/local.example.toml +16 -0
  13. anchorless-1.0.0/notes.md +1 -0
  14. anchorless-1.0.0/pyproject.toml +65 -0
  15. anchorless-1.0.0/scripts/release.sh +34 -0
  16. anchorless-1.0.0/src/anchorless/__init__.py +8 -0
  17. anchorless-1.0.0/src/anchorless/breaking.py +61 -0
  18. anchorless-1.0.0/src/anchorless/cli.py +230 -0
  19. anchorless-1.0.0/src/anchorless/config.py +573 -0
  20. anchorless-1.0.0/src/anchorless/examples.py +33 -0
  21. anchorless-1.0.0/src/anchorless/fonts/Barlow-Bold.ttf +0 -0
  22. anchorless-1.0.0/src/anchorless/fonts/Barlow-Medium.ttf +0 -0
  23. anchorless-1.0.0/src/anchorless/fonts/Barlow-Regular.ttf +0 -0
  24. anchorless-1.0.0/src/anchorless/fonts/Barlow-SemiBold.ttf +0 -0
  25. anchorless-1.0.0/src/anchorless/fonts/BarlowCondensed-Bold.ttf +0 -0
  26. anchorless-1.0.0/src/anchorless/fonts/BarlowCondensed-ExtraBold.ttf +0 -0
  27. anchorless-1.0.0/src/anchorless/fonts/BarlowCondensed-SemiBold.ttf +0 -0
  28. anchorless-1.0.0/src/anchorless/fonts/OFL-Barlow.txt +93 -0
  29. anchorless-1.0.0/src/anchorless/fonts/OFL-pixelifysans.txt +93 -0
  30. anchorless-1.0.0/src/anchorless/fonts/OFL-pressstart2p.txt +93 -0
  31. anchorless-1.0.0/src/anchorless/fonts/OFL-silkscreen.txt +93 -0
  32. anchorless-1.0.0/src/anchorless/fonts/OFL-vt323.txt +93 -0
  33. anchorless-1.0.0/src/anchorless/fonts/PixelifySans.ttf +0 -0
  34. anchorless-1.0.0/src/anchorless/fonts/PressStart2P-Regular.ttf +0 -0
  35. anchorless-1.0.0/src/anchorless/fonts/Silkscreen-Bold.ttf +0 -0
  36. anchorless-1.0.0/src/anchorless/fonts/Silkscreen-Regular.ttf +0 -0
  37. anchorless-1.0.0/src/anchorless/fonts/VT323-Regular.ttf +0 -0
  38. anchorless-1.0.0/src/anchorless/headline.py +201 -0
  39. anchorless-1.0.0/src/anchorless/models.py +95 -0
  40. anchorless-1.0.0/src/anchorless/netguard.py +74 -0
  41. anchorless-1.0.0/src/anchorless/newsroom.py +175 -0
  42. anchorless-1.0.0/src/anchorless/passwords.py +52 -0
  43. anchorless-1.0.0/src/anchorless/pipeline.py +159 -0
  44. anchorless-1.0.0/src/anchorless/render/__init__.py +22 -0
  45. anchorless-1.0.0/src/anchorless/render/crawl.py +87 -0
  46. anchorless-1.0.0/src/anchorless/render/pages.py +364 -0
  47. anchorless-1.0.0/src/anchorless/render/stills.py +66 -0
  48. anchorless-1.0.0/src/anchorless/render/studio.py +754 -0
  49. anchorless-1.0.0/src/anchorless/render/theme.py +180 -0
  50. anchorless-1.0.0/src/anchorless/settings.py +629 -0
  51. anchorless-1.0.0/src/anchorless/sound.py +241 -0
  52. anchorless-1.0.0/src/anchorless/sources/__init__.py +0 -0
  53. anchorless-1.0.0/src/anchorless/sources/rss.py +129 -0
  54. anchorless-1.0.0/src/anchorless/store.py +99 -0
  55. anchorless-1.0.0/src/anchorless/stream/__init__.py +0 -0
  56. anchorless-1.0.0/src/anchorless/stream/admin.py +187 -0
  57. anchorless-1.0.0/src/anchorless/stream/encoder.py +252 -0
  58. anchorless-1.0.0/src/anchorless/stream/guide.py +99 -0
  59. anchorless-1.0.0/src/anchorless/stream/playout.py +238 -0
  60. anchorless-1.0.0/src/anchorless/stream/preview.py +49 -0
  61. anchorless-1.0.0/src/anchorless/stream/server.py +556 -0
  62. anchorless-1.0.0/src/anchorless/stream/tsbuffer.py +148 -0
  63. anchorless-1.0.0/src/anchorless/stream/web.py +274 -0
  64. anchorless-1.0.0/src/anchorless/text.py +107 -0
  65. anchorless-1.0.0/src/anchorless/web/assets/settings.css +92 -0
  66. anchorless-1.0.0/src/anchorless/web/assets/settings.js +501 -0
  67. anchorless-1.0.0/src/anchorless/web/assets/style.css +179 -0
  68. anchorless-1.0.0/src/anchorless/web/assets/viewer.js +305 -0
  69. anchorless-1.0.0/src/anchorless/web/index.html +73 -0
  70. anchorless-1.0.0/src/anchorless/web/settings.html +46 -0
  71. anchorless-1.0.0/src/anchorless/web/vendor/mpegts.LICENSE +202 -0
  72. anchorless-1.0.0/src/anchorless/web/vendor/mpegts.min.js +9 -0
  73. anchorless-1.0.0/tests/__init__.py +0 -0
  74. anchorless-1.0.0/tests/conftest.py +30 -0
  75. anchorless-1.0.0/tests/fixtures/outlet_a.xml +18 -0
  76. anchorless-1.0.0/tests/fixtures/outlet_b.xml +15 -0
  77. anchorless-1.0.0/tests/test_config.py +117 -0
  78. anchorless-1.0.0/tests/test_headline.py +143 -0
  79. anchorless-1.0.0/tests/test_pipeline.py +79 -0
  80. anchorless-1.0.0/tests/test_settings.py +490 -0
  81. anchorless-1.0.0/tests/test_stream.py +345 -0
  82. anchorless-1.0.0/tests/test_text.py +43 -0
  83. anchorless-1.0.0/uv.lock +1076 -0
@@ -0,0 +1,8 @@
1
+ .venv
2
+ .git
3
+ data
4
+ out
5
+ tests
6
+ **/__pycache__
7
+ config.toml
8
+ .env
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ /data/
8
+ /out/
9
+ # Your live config holds your location and local sources: never commit it.
10
+ /config.toml
11
+ .env
12
+ *.mp4
13
+ *.ts
@@ -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"]
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/anchorless)](https://pypi.org/project/anchorless/)
33
+ [![Python versions](https://img.shields.io/pypi/pyversions/anchorless)](https://pypi.org/project/anchorless/)
34
+ [![Docker image](https://img.shields.io/badge/docker-ghcr.io-blue?logo=docker&logoColor=white)](https://github.com/GabeBecker2048/anchorless/pkgs/container/anchorless)
35
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/GabeBecker2048/anchorless/blob/main/LICENSE)
36
+ [![CI](https://github.com/GabeBecker2048/anchorless/actions/workflows/test.yml/badge.svg)](https://github.com/GabeBecker2048/anchorless/actions/workflows/test.yml)
37
+
38
+ ![The Studio look: an empty pixel-art news set. Headlines play on the big screen behind the desk, then a breaking-news alert turns the studio red](https://raw.githubusercontent.com/GabeBecker2048/anchorless/main/docs/studio.gif)
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
+ [![PyPI](https://img.shields.io/pypi/v/anchorless)](https://pypi.org/project/anchorless/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/anchorless)](https://pypi.org/project/anchorless/)
5
+ [![Docker image](https://img.shields.io/badge/docker-ghcr.io-blue?logo=docker&logoColor=white)](https://github.com/GabeBecker2048/anchorless/pkgs/container/anchorless)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/GabeBecker2048/anchorless/blob/main/LICENSE)
7
+ [![CI](https://github.com/GabeBecker2048/anchorless/actions/workflows/test.yml/badge.svg)](https://github.com/GabeBecker2048/anchorless/actions/workflows/test.yml)
8
+
9
+ ![The Studio look: an empty pixel-art news set. Headlines play on the big screen behind the desk, then a breaking-news alert turns the studio red](https://raw.githubusercontent.com/GabeBecker2048/anchorless/main/docs/studio.gif)
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.