- Python 89.1%
- Just 8.4%
- Dockerfile 2.5%
Co-authored-by: PatillaCode <patillacode@gmail.com> Co-committed-by: PatillaCode <patillacode@gmail.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| tests | ||
| .dockerignore | ||
| .env.sample | ||
| .gitignore | ||
| app.py | ||
| cache.py | ||
| detect.py | ||
| docker-compose.yml | ||
| Dockerfile | ||
| justfile | ||
| notify.py | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
yt-shorts-proxy
A self-hostable HTTP microservice that provides a Piped-compatible API for YouTube Shorts detection.
Built as a sidecar for FreshRSS with the xExtension-YouTubeChannel2RssFeed extension, as a drop-in replacement for the defunct Piped service.
Why this exists
FreshRSS's xExtension-YouTubeChannel2RssFeed extension can block YouTube Shorts from your
RSS feed, but it requires a 3rd_party_url pointing to a Piped-compatible API.
Piped shut down in 2024, leaving the Shorts-blocking feature broken.
This service replaces it, no API keys, no external dependencies, fully self-hosted.
What it does
Exposes three endpoints that the extension calls:
| Endpoint | When called | What it does |
|---|---|---|
GET /videos?part=short,contentDetails&id={videoId} |
Every new feed entry | Detects whether the video is a Short |
GET /channels?handle={handle} |
Adding a new @handle feed |
Resolves a YouTube handle to a channel ID |
GET /health |
Docker healthcheck | Returns {"status": "ok"} |
Results are cached in SQLite:
- Short status: cached permanently (a Short is always a Short)
- Channel handles: cached for 24 hours
How Short detection works
Uses the YouTube oEmbed API:
GET https://www.youtube.com/oembed?url=https://www.youtube.com/shorts/{id}&format=json
The response includes thumbnail dimensions. Shorts are always portrait (height > width);
regular videos are landscape. This approach bypasses EU consent walls and requires no
YouTube API key.
How channel resolution works
Fetches https://www.youtube.com/@{handle} and extracts the channel ID via regex from the
page HTML.
Falls back to a secondary regex on /channel/UC... URLs if the primary match fails.
Setup
Prerequisites
- Docker and Docker Compose
As a FreshRSS sidecar (main purpose)
Add the service to your existing FreshRSS compose.yml:
services:
# ... your existing freshrss service ...
yt-shorts-proxy:
image: forgejo.patilla.es/patillacode/yt-shorts-proxy:latest
container_name: yt-shorts-proxy
restart: unless-stopped
volumes:
- ./yt-shorts-cache:/data # SQLite cache volume
env_file:
- yt-shorts-proxy.env # copy from .env.sample
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "2"
# No ports needed, FreshRSS reaches it via the internal Docker network
Create the env file:
cp /path/to/yt-shorts-proxy/.env.sample /path/to/freshrss/yt-shorts-proxy.env
# Edit with your values (NTFY_URL, Telegram credentials, etc.)
Start the sidecar:
cd /path/to/freshrss
mkdir -p yt-shorts-cache
docker compose up -d yt-shorts-proxy
docker compose logs yt-shorts-proxy
# yt-shorts-proxy listening on :8080
Verify FreshRSS can reach it:
docker exec freshrss wget -qO- http://yt-shorts-proxy:8080/health
# {"status": "ok"}
Standalone (for testing)
mkdir -p yt-shorts-test && cd yt-shorts-test
curl -O https://forgejo.patilla.es/patillacode/yt-shorts-proxy/raw/branch/main/docker-compose.yml
curl -O https://forgejo.patilla.es/patillacode/yt-shorts-proxy/raw/branch/main/.env.sample
cp .env.sample .env
mkdir -p data
# Uncomment the ports section in docker-compose.yml first
docker compose up -d
curl http://localhost:8080/health
# {"status": "ok"}
Configure the FreshRSS extension
- Log into FreshRSS
- Go to Extensions → YouTubeChannel2RssFeed → Configure
- Set 3rd party URL to:
http://yt-shorts-proxy:8080 - Set Shorts to
block(ormark_as_readif you prefer) - Set Short duration to
120(seconds, fallback detection threshold) - Save
After the next feed refresh, Shorts will be blocked. Check FreshRSS logs for confirmation:
docker exec freshrss grep 'YouTubeChannel2RssFeed' /var/www/FreshRSS/data/users/_/log.txt | tail -20
# YouTubeChannel2RssFeed: short blocked (id=...)
Environment variables
Copy .env.sample to .env and edit as needed. All variables are optional, defaults work out of the box.
Server
| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
Port the HTTP server listens on |
DB_PATH |
/data/cache.db |
SQLite cache file path. Mount /data as a volume to persist across restarts. |
YouTube requests
| Variable | Default | Description |
|---|---|---|
USER_AGENT |
Chrome 124 on Linux | User-Agent header for YouTube requests. Change if YouTube starts rejecting the default. |
REQUEST_DELAY |
1.0 |
Minimum seconds between outbound YouTube requests. Increase if you see 429 responses. |
Error notifications
| Variable | Default | Description |
|---|---|---|
ERROR_THRESHOLD |
5 |
Number of failures within ERROR_WINDOW seconds before a notification fires |
ERROR_WINDOW |
300 |
Rolling window in seconds for counting errors |
ERROR_COOLDOWN |
3600 |
Minimum seconds between notifications (prevents spam) |
SERVICE_NAME |
yt-shorts-proxy |
Name shown in notification messages |
ntfy (optional)
Set NTFY_URL to enable. Leave unset to disable ntfy notifications.
| Variable | Default | Description |
|---|---|---|
NTFY_URL |
(unset) | ntfy topic URL, e.g. https://ntfy.sh/your-topic or http://ntfy.yourdomain.com/topic |
NTFY_PRIORITY |
high |
Message priority: min, low, default, high, urgent |
Telegram (optional)
Set both TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID to enable. Leave either unset to disable.
| Variable | Default | Description |
|---|---|---|
TELEGRAM_BOT_TOKEN |
(unset) | Your Telegram bot token (from @BotFather) |
TELEGRAM_CHAT_ID |
(unset) | Chat ID to send notifications to |
Notification example
When error threshold is reached:
[yt-shorts-proxy] 5 detection errors in 300s
Last error: HTTP 429 on video dQw4w9WgXcW
https://www.youtube.com/watch?v=dQw4w9WgXcW
Both ntfy and Telegram can be active simultaneously.
Development
Requirements: Python 3.12+, uv, just
git clone https://forgejo.patilla.es/patillacode/yt-shorts-proxy.git
cd yt-shorts-proxy
uv sync --group dev
just test
Available commands:
just test # run all tests
just test-k cache # run tests matching a pattern
just run # run locally (uses ./data/cache.db)
just lint # ruff check
just fmt # ruff format
just check # lint + format check (CI)
just docker-build # build Docker image locally
just docker-run # run container on port 8080
just pr # print the current branch PR URL
just release v1.2.3 # tag, push, and print the Forgejo release URL
Branch strategy and release flow
main production, protected, Docker images built from here
feature/* short-lived branches, merged into main via PR
- Branch off
main, make changes, open a PR back intomainwithjust pr - Once merged into
main, cut a release:
just release v0.3.0
# Then open the printed URL to create the release on Forgejo
CI
Two Forgejo Actions workflows run automatically:
| Workflow | Trigger | What it does |
|---|---|---|
test.yml |
Push or PR to main |
Runs the full pytest suite |
docker.yml |
Push of a v* tag |
Runs tests, then builds and pushes the Docker image |
The Docker build only runs if tests pass.
The image is published to the Forgejo container registry:
forgejo.patilla.es/patillacode/yt-shorts-proxy:latest
forgejo.patilla.es/patillacode/yt-shorts-proxy:v0.1.0
Pull it with:
docker pull forgejo.patilla.es/patillacode/yt-shorts-proxy:latest
License
MIT