self-hostable Piped-compatible API for YouTube Shorts detection. FreshRSS sidecar.
  • Python 89.1%
  • Just 8.4%
  • Dockerfile 2.5%
Find a file
PatillaCode 93e4245874
All checks were successful
Tests / test (push) Successful in 16s
Build and Push Docker Image / test (push) Successful in 47s
Build and Push Docker Image / build-and-push (push) Successful in 59s
feature/oembed-detection-fix (#3)
Co-authored-by: PatillaCode <patillacode@gmail.com>
Co-committed-by: PatillaCode <patillacode@gmail.com>
2026-04-09 15:01:51 +02:00
.forgejo/workflows ci: rewrite workflows using catthehacker runner and standard actions 2026-04-09 13:03:06 +02:00
tests feature/oembed-detection-fix (#3) 2026-04-09 15:01:51 +02:00
.dockerignore feat: project scaffold — uv, justfile, env config 2026-04-08 17:12:14 +02:00
.env.sample feat: project scaffold — uv, justfile, env config 2026-04-08 17:12:14 +02:00
.gitignore feat: project scaffold — uv, justfile, env config 2026-04-08 17:12:14 +02:00
app.py feat: HTTP routing for /videos, /channels, /health 2026-04-08 17:31:11 +02:00
cache.py feat: SQLite cache for video and channel lookups 2026-04-08 17:16:59 +02:00
detect.py feature/oembed-detection-fix (#3) 2026-04-09 15:01:51 +02:00
docker-compose.yml feature/oembed-detection-fix (#3) 2026-04-09 15:01:51 +02:00
Dockerfile feat: Docker setup with official uv image and lockfile 2026-04-08 17:38:57 +02:00
justfile feature/oembed-detection-fix (#3) 2026-04-09 15:01:51 +02:00
notify.py feat: add NTFY_TOKEN support for Bearer auth (#1) (#2) 2026-04-09 13:31:14 +02:00
pyproject.toml feat: project scaffold — uv, justfile, env config 2026-04-08 17:12:14 +02:00
README.md feature/oembed-detection-fix (#3) 2026-04-09 15:01:51 +02:00
uv.lock feat: Docker setup with official uv image and lockfile 2026-04-08 17:38:57 +02:00

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

  1. Log into FreshRSS
  2. Go to Extensions → YouTubeChannel2RssFeed → Configure
  3. Set 3rd party URL to: http://yt-shorts-proxy:8080
  4. Set Shorts to block (or mark_as_read if you prefer)
  5. Set Short duration to 120 (seconds, fallback detection threshold)
  6. 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
  1. Branch off main, make changes, open a PR back into main with just pr
  2. 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