Skip to content

Repository files navigation

Omni Notify

Monitors YouTube, Twitch, and Kick channels and sends Pushover notifications when they go live or offline. Optionally runs AI-powered briefing agents that search the web on a schedule and send notification summaries.

Quick Start

services:
  omni-notify:
    image: ghcr.io/micthiesen/omni-notify:latest
    environment:
      - PUSHOVER_TOKEN=xxx
      - PUSHOVER_USER=xxx
      - KICK_CLIENT_ID=xxx # only if monitoring Kick channels
      - KICK_CLIENT_SECRET=xxx
    volumes:
      - ./channels.json:/app/channels.json:ro
    restart: unless-stopped

Channels are configured in channels.json (path overridable via CHANNELS_CONFIG_PATH), keyed by display name, with platform usernames and per-streamer options inline:

{
  "MKBHD": { "youtube": "@mkbhd" },
  "Destiny": { "youtube": "@destiny", "kick": "destiny" },
  "Shroud": { "twitch": "shroud", "tier": "background" }
}

Platform fields are youtube / twitch / kick; each takes one username or an array of them. An invalid file fails startup with a validation error rather than silently dropping config.

One entry = one streamer. A streamer live on multiple platforms gets one "went live" notification when they start streaming anywhere and one "went offline" notification when all platforms go offline — no double-pings for multistreams.

How It Works

Checks every 20 seconds (with random jitter) whether monitored channels are live. Sends a notification on aggregate status transitions (offline-everywhere → live-anywhere, or live-anywhere → offline-everywhere). State is persisted in SQLite so it survives restarts.

  • YouTube: Scrapes the channel's /live page HTML. No API key needed, but could break if YouTube changes its page structure.
  • Twitch: Uses Twitch's public GraphQL API. No authentication required. More stable than YouTube scraping.
  • Kick: Uses Kick's official public API (api.kick.com/public/v1/channels). Requires registering an app at dev.kick.com and providing KICK_CLIENT_ID + KICK_CLIENT_SECRET (scope: channel:read). The app-only access token is cached and refreshed automatically.

Primary binding. When a streamer is live on multiple platforms, one binding is chosen as the "primary" for the notification URL and title. The first platform to go live wins, and sticks for the rest of the session. If they go live simultaneously, priority order is YouTube → Twitch → Kick.

Set OFFLINE_NOTIFICATIONS=false to only get notified when channels go live.

Per-Streamer Options

Alongside the platform fields, each channels.json entry accepts:

  • pushoverToken: override the Pushover token for this streamer's notifications.
  • tier: set to "background" for second-tier streamers you check on the dashboard but never want pushed about. Mutes live/offline/title-change notifications, restricts viewer-record notifications to all-time highs only, and polls at a relaxed cadence (every 60s instead of 20s). Tracking, the dashboard, and /api/trigger-channels are unaffected. Combining it with an explicit liveNotifications is a config error.
  • liveNotifications: set to false to mute live/offline/title-change notifications while keeping full-rate polling (e.g. for external integrations that need fast state). Viewer-record notifications (7d/30d/90d/all-time highs) still fire.

Title-change notifications are eagerly debounced: the first change fires immediately, further changes within 10 minutes are held with the last one winning.

Briefing Agents

AI agents that search the web on a schedule and send notification summaries. Requires TAVILY_API_KEY, BRIEFINGS_PATH, and an API key for your chosen model provider.

Create .md files in your briefings folder:

---
schedule: "0 0 8 * * *"
---
You are a morning news assistant. Today is {{date}}, {{time}}.

{{history:10}}

Search for the most important news from the past 24 hours.
Do not cover topics from past notifications above.
  • Filename becomes the task name (CanadianNews.md registers as "CanadianNews")
  • schedule is a 6-field node-cron expression (with seconds)
  • The body is the prompt sent to the AI agent

Placeholders

Placeholder Description Example
{{date}} Current date (local timezone) Thursday, February 6, 2026
{{time}} Current time (local timezone) 9:00 AM EST
{{history:N}} Last N notifications from this briefing (titles + URLs)

History is stored per-briefing in SQLite and auto-pruned to the last 50 entries.

Media Recommendations

A scheduled pipeline (default: Mon/Wed/Fri at 5pm) that picks at most one movie or TV title per run, acquires missing titles through Radarr or Sonarr, and sends a Pushover notification explaining the pick. Titles already available in Plex are recommended without another acquisition request.

Each run:

  1. Polls Plex history, series-level progress, local availability, and the Radarr/Sonarr tracked catalog. It labels passive outcomes (started, watched, abandoned, ignored) and incorporates explicit "good pick" or "not for me" feedback.
  2. Builds a candidate pool from TMDB (recommendations seeded by recent watches, genre discovery, trending, plus a novelty bucket outside your usual genres).
  3. Hard-filters in code: anything watched, in progress, tracked by Radarr/Sonarr, explicitly rejected, or recently recommended is dropped before a model sees it.
  4. Enriches candidates with structured TMDB commitment and creative metadata, then scores the pool with a cheap model (RECS_SHORTLIST_MODEL) and keeps the top 5.
  5. Researches the finalists with web search, then a strong model (RECS_SELECTION_MODEL) picks exactly one title or decides to add nothing that day.

Plex is the source of watch history, in-progress state, and local availability. Radarr handles movie acquisition and Sonarr handles TV acquisition. All reads fail closed: an unavailable service skips the run instead of treating missing state as an empty library.

A separate weekly TasteReflection task maintains a versioned taste profile. It converts Plex observations and recommendation outcomes into an idempotent evidence ledger, computes behavioral statistics, and performs a bounded draft-and-critic reflection. Every learned claim must cite stored evidence. The latest profile is added to recommendation context and shown in the UI; code, prompts, and scoring rules are never self-modified. If no evidence changed, reflection exits without a model call.

See the recommendation review checkpoint for the decisions intentionally deferred until enough real recommendations have outcomes.

Podcast Recommendations

A sibling pipeline (default: Mon/Wed/Fri at 11am, enabled by setting PODCAST_TASTE_PATH) that recommends fresh podcast episodes from shows you don't already follow, and sends a Pushover notification per pick. It's people-first: the main goal is surfacing episodes where a voice you follow guests somewhere new (see docs/podcast-recs.md).

Each run:

  1. Reads subscribed shows and listen history from the Castro account (see docs/castro-sync.md). Subscribed shows are excluded and double as taste evidence alongside the seed profile and explicit feedback; a failed account read aborts the run. Without Castro credentials it runs off the seed profile and feedback alone.
  2. Tier 1 — guest appearances. For a rotating batch of the people in your profile's ## Voices list, finds recent episodes featuring them as guests via Podcast Index (byperson, free) with a Tavily person-search fallback for non-podcasters. A model gate default-includes them (following the person is the signal), capped so a press-tour week can surface several.
  3. Tier 2 — topic/drama. Multi-angle web search → cheap shortlist → strong-model one-pick, conservative and suppressed once Tier 1 delivered enough.
  4. Verifies every candidate's release date against the show's actual RSS feed (or Podcast Index's resolved data), and hard-filters in code: older than 7 days, already recommended, show on 30-day cooldown, rejected, or subscribed.

Recommended episodes are never repeated. When Castro credentials are set, listen history labels outcomes (listened / abandoned / ignored) automatically and each selected episode is resolved by RSS URL and added to the end of the Castro queue before notification. Resolution or enqueue failure falls back safely to the recommendation deep link. The good-pick/not-for-me feedback buttons in the web UI are always available.

The same Castro credentials enable an independent CastroInboxCleanup task. It runs hourly and silently clears Inbox episodes whose description begins with This is a free preview, the standard marker used by Substack preview episodes. It changes only the Inbox is_new state and never removes an episode from the Queue. Matching is deliberately case-sensitive and prefix-only.

PressPods (Article → Podcast)

Enabled by setting PRESSPODS_AUTH_TOKEN (plus a Google API key and a TTS backend — either a self-hosted Higgs server via PRESSPODS_TTS_URL, or ELEVENLABS_API_KEY with PRESSPODS_TTS_PROVIDER=elevenlabs). Submit an article URL — from an iOS Shortcut via POST /pods/episodes?authToken=… with { "url": "…" }, or from the /pods page — and it becomes an episode in a private podcast RSS feed (GET /pods/rss?authToken=…) your podcast app subscribes to.

Each submission becomes a durable job (visible on /pods, with retries and per-run logs) processed by the PressPods task:

  1. Seven article retrievers run in parallel (Postlight, Readability, Extractus, Wayback, removepaywall, raw fetch, and Jina.ai when JINA_API_KEY is set); an LLM rates each result's extraction quality and the best wins.
  2. A broadcast-style cleaning pass rewrites the article for the ear (one-idea sentences, attribution-first quotes, number rounding, a cold-open hook and a spoken outro) and marks major sections for chapters.
  3. The configured TTS backend (self-hosted Higgs v3, or ElevenLabs v3) synthesizes each chunk separately, with a length-verify retry for the local model; ffmpeg denoises (Higgs), per-chunk-levels, two-pass loudness-normalizes to -16 LUFS, and joins the intro jingle click-free; the lead image and chapter markers are embedded as ID3 tags.
  4. The MP3 is stored on disk (next to the SQLite DB by default) and served at /pods/audio/<id>.mp3 with an unguessable content-addressed name; a Pushover notification announces the episode.

Transient failures (TTS 429/5xx, network blips) retry automatically with backoff; permanent failures surface on /pods with a retry button. Submitted articles are also bookmarked in Karakeep when KARAKEEP_URL/KARAKEEP_API_KEY are set. To expose the feed publicly, reverse-proxy just the /pods/* paths and set PRESSPODS_PUBLIC_URL to the public origin (or let it derive from X-Forwarded-* headers).

Web UI

The built-in server (port FRONTEND_PORT, default 3000) serves the Omni Notify dashboard:

  • / shows live streamer status (who's live now, title, uptime, peak viewers), a stat strip, every scheduled task with its cron schedule, ticking next-run countdown, "Run now" button and expandable run history, plus a recent-activity feed with per-task filtering.
  • /pets is the pet weight tracker.
  • /recommendations lists every recommendation with poster, status, reasoning, service links, explicit feedback controls, filters, the current evidence-backed taste profile, and recent pipeline activity.
  • /podcasts lists podcast episode recommendations with show artwork, status filters, episode/discussion links, good-pick/not-for-me feedback controls, and the podcast taste profile.
  • /feedback/recommendations/:id and /feedback/podcasts/:id are mobile-first one-tap rating pages. Pushover recommendation notifications deep-link here ("Rate this pick"), and the page links onward to the full recommendation view.
  • /pods lists PressPods episodes with an inline player, costs, and processing logs, plus a submit-URL form and retry controls for failed jobs.
  • /briefings is a browsable archive of briefing notifications (the last 50 stored per briefing).
  • /emails shows what the parcel and calendar email pipelines did with each email (why it was admitted or filtered, per-item results, honest processed/partial/failed outcomes) with per-email processing logs, one-click reprocess, block-sender and not-relevant/missed feedback actions, a forget-tracking-number escape hatch, and a user-editable sender-rules section. A shared LLM triage call gates both pipelines; corrections feed back into its prompt.

Updates are pushed in realtime over SSE (/api/events) on the same HTTP port — no extra ports needed; the UI falls back to polling /api/snapshot (and shows a "Reconnecting" badge) if the stream drops. Task runs are persisted in SQLite (last 50 per task) so history survives restarts.

To iterate on the frontend without real credentials, src/tools/preview-server.ts boots the real server with fake tasks, streamers, runs, recommendations, and a pet:

DB_NAME=/tmp/omni-preview.db FRONTEND_PORT=3999 npx tsx src/tools/preview-server.ts

AI Model Configuration

Models are configured via environment variables using provider:model format. Supported providers: google, anthropic, openai. You only need an API key for the provider you're using.

Variable Default Used for
BRIEFING_MODEL openai:gpt-5.6-luna Briefing agents
EXTRACTION_MODEL openai:gpt-5.6-luna Parcel email extraction
CALENDAR_EXTRACTION_MODEL openai:gpt-5.6-terra Calendar email extraction
TRIAGE_MODEL openai:gpt-5.6-luna Shared email relevance triage
RECS_SHORTLIST_MODEL openai:gpt-5.6-luna Recommendation shortlist scoring
RECS_SELECTION_MODEL openai:gpt-5.6 Recommendation research + final pick
TASTE_REFLECTION_MODEL openai:gpt-5.6-luna Weekly evidence-backed taste reflection
PRESSPODS_METADATA_MODEL openai:gpt-5.6-luna PressPods per-retriever metadata + rating
PRESSPODS_CLEANING_MODEL openai:gpt-5.6-terra PressPods narration rewrite

Examples:

BRIEFING_MODEL=openai:gpt-5.6-luna
BRIEFING_MODEL=anthropic:claude-sonnet-5
BRIEFING_MODEL=google:gemini-3.5-flash

Environment Variables

Variable Required Description
PUSHOVER_USER Yes Pushover user key
PUSHOVER_TOKEN Yes Pushover app token
KICK_CLIENT_ID No Kick OAuth client ID (dev.kick.com); required when channels.json has Kick channels
KICK_CLIENT_SECRET No Kick OAuth client secret
OFFLINE_NOTIFICATIONS No Send offline notifications (default: true)
BRIEFING_MODEL No AI model for briefings (default: openai:gpt-5.6-luna)
EXTRACTION_MODEL No AI model for parcel email extraction (default: openai:gpt-5.6-luna)
CALENDAR_EXTRACTION_MODEL No AI model for calendar email extraction (default: openai:gpt-5.6-terra)
TRIAGE_MODEL No AI model for shared email triage (default: openai:gpt-5.6-luna)
EMAIL_TRANSPORT No Email transport: fastmail (JMAP) or icloud (IMAP). Defaults to fastmail while FASTMAIL_API_TOKEN is set, else icloud when its credentials exist.
CALDAV_PROVIDER No CalDAV backend for calendar-event writes: fastmail or icloud (default: follows EMAIL_TRANSPORT)
EMAIL_SELF_ADDRESS No Own receiving address for self-sent-mail filtering (default: FASTMAIL_USERNAME)
FASTMAIL_API_TOKEN No Fastmail JMAP API token (enables the Fastmail email transport)
FASTMAIL_APP_PASSWORD / FASTMAIL_USERNAME No Fastmail CalDAV credentials
FASTMAIL_CALENDAR_ID No Pins the Fastmail calendar (default: auto-discover)
ICLOUD_USERNAME No iCloud primary username (user@icloud.com, not the custom-domain address); IMAP + CalDAV
ICLOUD_APP_PASSWORD No iCloud app-specific password (IMAP + CalDAV)
ICLOUD_CALENDAR_NAME No Display name of the iCloud calendar to write to (e.g. Personal)
ICLOUD_CALENDAR_URL No Pins the iCloud calendar collection URL (default: RFC 6764 discovery)
GOOGLE_GENERATIVE_AI_API_KEY No Required for google: models
ANTHROPIC_API_KEY No Required for anthropic: models
OPENAI_API_KEY No Required for openai: models
TAVILY_API_KEY No Tavily web search (required for briefings + recommendations)
BRIEFINGS_PATH No Folder containing .md briefing configs
CHANNELS_CONFIG_PATH No Path to channels.json (default: ./channels.json)
TMDB_API_KEY No TMDB API key (required for recommendations; v3 key or v4 read token)
RECS_SCHEDULE No Recommendation cron (default: 0 0 17 * * 1,3,5)
TASTE_REFLECTION_MODEL No Model for evidence-backed taste reflection (default: openai:gpt-5.6-luna)
TASTE_REFLECTION_SCHEDULE No Taste-profile reflection cron (default: 0 0 4 * * 0, Sunday 4am)
RECS_PUBLIC_URL No Public/LAN Omni base URL used by notification links (default: http://omni.boris)
PUSHOVER_RECS_TOKEN No Pushover token for recommendations (falls back to PUSHOVER_TOKEN)
PODCAST_TASTE_PATH No Markdown listener profile (required to enable podcast recommendations)
PODCAST_RECS_SCHEDULE No Podcast recommendation cron (default: 0 0 11 * * 1,3,5)
PUSHOVER_PODCAST_TOKEN No Pushover token for podcast recs (falls back to PUSHOVER_TOKEN)
CASTRO_ACCESS_ID / CASTRO_SECRET_KEY No Castro device credentials (account reads, queue writes, and hourly Inbox cleanup)
PODCASTINDEX_KEY / PODCASTINDEX_SECRET No Podcast Index API (guest-appearance discovery; quote the secret — it contains #)
PODCAST_VOICE_ROTATION_MAX / PODCAST_MAX_GUEST_PICKS No Voices searched per run (default 12) / Tier-1 guest cap (default 6)
PODCAST_TASTE_REFLECTION_MODEL No Model for weekly podcast taste reflection (default: openai:gpt-5.6-luna)
PODCAST_TASTE_REFLECTION_SCHEDULE No Podcast taste reflection cron (default: 0 0 5 * * 0, Sunday 5am)
PRESSPODS_AUTH_TOKEN No Long random secret; enables PressPods and authenticates /pods/episodes + /pods/rss
PRESSPODS_TTS_PROVIDER No TTS backend: higgs (self-hosted, default) or elevenlabs
PRESSPODS_TTS_URL For higgs mlx-audio server URL, e.g. http://10.10.1.90:8000
PRESSPODS_TTS_MODEL No Higgs model repo (default bosonai/higgs-audio-v3-tts-4b)
ELEVENLABS_API_KEY For elevenlabs ElevenLabs v3 TTS
ELEVENLABS_VOICE_MALE / ELEVENLABS_VOICE_FEMALE No Voice-id overrides for narration (default: Brian / Matilda)
PRESSPODS_PUBLIC_URL No Public origin for RSS enclosure URLs (else derived from forwarded headers)
PRESSPODS_AUDIO_DIR No Episode MP3 directory (default: press-pods-audio next to the DB)
JINA_API_KEY No Enables the Jina.ai Reader retriever
PUSHOVER_PRESSPODS_TOKEN No Pushover token for PressPods (falls back to PUSHOVER_TOKEN)
KARAKEEP_URL / KARAKEEP_API_KEY No Bookmark submitted articles in Karakeep
PLEX_URL / PLEX_TOKEN For recommendations Plex server URL and token
PLEX_ACCOUNT_ID For shared Plex servers Account ID used to scope viewing history; multiple detected accounts fail closed without it
RADARR_URL / RADARR_API_KEY For recommendations Radarr v3 API connection
RADARR_ROOT_FOLDER_PATH / RADARR_QUALITY_PROFILE_ID For recommendations Defaults for acquired movies
SONARR_URL / SONARR_API_KEY For recommendations Sonarr v3 API connection
SONARR_ROOT_FOLDER_PATH / SONARR_QUALITY_PROFILE_ID For recommendations Defaults for acquired series
LOG_LEVEL No debug, info, warn, or error

Development

pnpm dev        # Development with hot reload
pnpm build      # TypeScript compilation
pnpm test       # Run tests (vitest)
pnpm check      # Biome linting + formatting

Inspired by youtube_live_alert.

About

Monitors YouTube, Twitch, and Kick channels and sends Pushover notifications when they go live or offline

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages