Compare commits

...

7 Commits

Author SHA1 Message Date
194ed3b87f Add TLC Search app to stack
Some checks failed
Deploy Stacks / deploy-prod (push) Has been skipped
Deploy Stacks / deploy-dev (push) Has been cancelled
Complete TLC stack now includes:
- rss-bridge: YouTube to RSS converter
- feed-master: RSS feed aggregator
- search: TLC Search Flask app (port 8088)

Requires Elasticsearch and Qdrant backends configured in .env

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 15:22:09 -05:00
0cadd5d8d4 Add TLC stack (RSS Bridge + Feed Master)
Some checks failed
Deploy Stacks / deploy-prod (push) Has been skipped
Deploy Stacks / deploy-dev (push) Has been cancelled
TLC provides YouTube RSS feed aggregation:
- rss-bridge: Converts YouTube channels to RSS feeds
- feed-master: Aggregates multiple RSS feeds into unified feed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 15:17:00 -05:00
32a4d728ef Trigger dev workflow
Some checks failed
Deploy Stacks / deploy-prod (push) Has been skipped
Deploy Stacks / deploy-dev (push) Has been cancelled
2026-01-22 15:07:04 -05:00
0d9b0302a2 Add stack-type=dev-only labels to dev stacks
Some checks failed
Deploy Stacks / deploy-prod (push) Has been skipped
Deploy Stacks / deploy-dev (push) Has been cancelled
Label stacks that should only run on ubuntu-dev:
- bookclub, dockge, filebrowser, meshmon, meshtastic-web
- mllogwatcher, network-mcp, obby, obsidian-tools
- syncthing, szurubooru

These will now be filtered by the updated workflow.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 15:05:36 -05:00
d44a8c7276 Sync dev workflow with master label-based filtering
Dev branch now only deploys stacks with stack-type=dev-only label,
matching the master workflow's approach.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 15:04:35 -05:00
f19577fbcc Trigger dev runner
Some checks failed
Deploy Stacks / deploy-prod (push) Has been skipped
Deploy Stacks / deploy-dev (push) Failing after 3s
2026-01-22 14:34:13 -05:00
9f61b06592 Update invidious stack to use companion approach
- Replace inv_sig_helper with invidious-companion for better YouTube API handling
- Add healthcheck for main container
- Add resource limits for all containers
- Add SQL init scripts for fresh database setup
- Update README with invidious secrets documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 12:52:19 -05:00
28 changed files with 612 additions and 30 deletions

View File

@@ -27,15 +27,32 @@ jobs:
STACKS=$(git diff --name-only HEAD~1 HEAD 2>/dev/null | grep '^stacks/' | cut -d'/' -f2 | sort -u || echo "")
if [ -z "$STACKS" ]; then
echo "No stacks changed, deploying all..."
echo "No stacks changed, checking all stacks..."
STACKS=$(ls stacks/)
fi
echo "Deploying: $STACKS"
echo "Evaluating stacks: $STACKS"
echo ""
for stack in $STACKS; do
COMPOSE_FILE="stacks/$stack/docker-compose.yml"
# Check stack-type label
STACK_TYPE=$(grep -o 'stack-type=[^"]*' "$COMPOSE_FILE" 2>/dev/null | head -1 | cut -d= -f2)
if [ -z "$STACK_TYPE" ]; then
echo "⚠️ SKIP $stack - no stack-type label found"
continue
fi
# On prod, only deploy 'prod' and 'public' stacks
if [ "$STACK_TYPE" != "prod" ] && [ "$STACK_TYPE" != "public" ]; then
echo "⏭️ SKIP $stack - stack-type=$STACK_TYPE (not for prod)"
continue
fi
echo "=========================================="
echo "Deploying $stack..."
echo "Deploying $stack (stack-type=$STACK_TYPE)..."
echo "=========================================="
STACK_DIR="${{ env.STACKS_DIR }}/$stack"
@@ -44,9 +61,10 @@ jobs:
# Copy files
sudo cp -r stacks/$stack/* "$STACK_DIR/"
# Create .env from template if exists
if [ -f "$STACK_DIR/.env.template" ]; then
sudo envsubst < "$STACK_DIR/.env.template" > "$STACK_DIR/.env"
# Create .env from template if .env doesn't exist
if [ -f "$STACK_DIR/.env.template" ] && [ ! -f "$STACK_DIR/.env" ]; then
echo "Creating .env from template..."
sudo sh -c "DOMAIN=$DOMAIN envsubst < '$STACK_DIR/.env.template' > '$STACK_DIR/.env'"
fi
# Deploy
@@ -59,11 +77,11 @@ jobs:
done
- name: Show running containers
run: sudo docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | head -20
run: sudo docker ps --format "table {{.Names}}\t{{.Status}}" | head -30
deploy-dev:
if: ${{ github.ref == 'refs/heads/dev' }}
runs-on: ubuntu-dev
runs-on: ubuntu-dev:host
steps:
- uses: actions/checkout@v4
with:
@@ -77,15 +95,32 @@ jobs:
STACKS=$(git diff --name-only HEAD~1 HEAD 2>/dev/null | grep '^stacks/' | cut -d'/' -f2 | sort -u || echo "")
if [ -z "$STACKS" ]; then
echo "No stacks changed, deploying all..."
echo "No stacks changed, checking all stacks..."
STACKS=$(ls stacks/)
fi
echo "Deploying: $STACKS"
echo "Evaluating stacks: $STACKS"
echo ""
for stack in $STACKS; do
COMPOSE_FILE="stacks/$stack/docker-compose.yml"
# Check stack-type label
STACK_TYPE=$(grep -o 'stack-type=[^"]*' "$COMPOSE_FILE" 2>/dev/null | head -1 | cut -d= -f2)
if [ -z "$STACK_TYPE" ]; then
echo "⚠️ SKIP $stack - no stack-type label found"
continue
fi
# On dev, only deploy 'dev-only' stacks
if [ "$STACK_TYPE" != "dev-only" ]; then
echo "⏭️ SKIP $stack - stack-type=$STACK_TYPE (not for dev)"
continue
fi
echo "=========================================="
echo "Deploying $stack..."
echo "Deploying $stack (stack-type=$STACK_TYPE)..."
echo "=========================================="
STACK_DIR="${{ env.STACKS_DIR }}/$stack"
@@ -94,9 +129,10 @@ jobs:
# Copy files
sudo cp -r stacks/$stack/* "$STACK_DIR/"
# Create .env from template if exists
if [ -f "$STACK_DIR/.env.template" ]; then
sudo envsubst < "$STACK_DIR/.env.template" > "$STACK_DIR/.env"
# Create .env from template if .env doesn't exist
if [ -f "$STACK_DIR/.env.template" ] && [ ! -f "$STACK_DIR/.env" ]; then
echo "Creating .env from template..."
sudo sh -c "DOMAIN=$DOMAIN envsubst < '$STACK_DIR/.env.template' > '$STACK_DIR/.env'"
fi
# Deploy
@@ -109,4 +145,4 @@ jobs:
done
- name: Show running containers
run: sudo docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | head -20
run: sudo docker ps --format "table {{.Names}}\t{{.Status}}" | head -30

View File

@@ -18,6 +18,7 @@ stacks/
├── perilous/ # Blog/website
├── ramz/ # Go web app
├── bookclub/ # Form mailer
├── invidious/ # YouTube frontend
├── watchtower/ # Auto container updates
├── dockge/ # Container management UI
└── smokeping/ # Network monitoring
@@ -96,6 +97,13 @@ Set these in Gitea → Repository → Settings → Actions → Secrets:
|--------|-------------|
| `PERILOUS_CODE_SERVER_PASSWORD` | Code-server password |
### Invidious
| Secret | Description |
|--------|-------------|
| `INVIDIOUS_DB_PASSWORD` | PostgreSQL password |
| `INVIDIOUS_HMAC_KEY` | Generate: `openssl rand -hex 16` |
| `INVIDIOUS_COMPANION_KEY` | Generate: `openssl rand -hex 16` (must match companion) |
## Runner Setup
The workflow requires a self-hosted runner on the prod server:

View File

@@ -6,6 +6,7 @@ services:
env_file:
- .env
labels:
- "com.ghost.tel/stack-type=dev-only"
- "traefik.enable=true"
- "traefik.http.routers.form-mailer.rule=Host(`bookclub.${DOMAIN}`)"
- "traefik.http.routers.form-mailer.entrypoints=https"

View File

@@ -14,6 +14,7 @@ services:
networks:
- web
labels:
- "com.ghost.tel/stack-type=dev-only"
- "traefik.enable=true"
- "traefik.http.routers.dockge.rule=Host(`dockge.${DOMAIN}`)"
- "traefik.http.routers.dockge.entrypoints=https"
@@ -23,3 +24,4 @@ services:
networks:
web:
external: true

View File

@@ -11,6 +11,7 @@ services:
networks:
- web
labels:
- "com.ghost.tel/stack-type=dev-only"
- "traefik.enable=true"
- "traefik.http.routers.filebrowser.entrypoints=https"
- "traefik.http.routers.filebrowser.rule=Host(`files.${DOMAIN}`)"

View File

@@ -1,5 +1,4 @@
DOMAIN=${DOMAIN}
INVIDIOUS_DB_PASSWORD=${INVIDIOUS_DB_PASSWORD}
INVIDIOUS_VISITOR_DATA=${INVIDIOUS_VISITOR_DATA}
INVIDIOUS_PO_TOKEN=${INVIDIOUS_PO_TOKEN}
INVIDIOUS_HMAC_KEY=${INVIDIOUS_HMAC_KEY}
INVIDIOUS_COMPANION_KEY=${INVIDIOUS_COMPANION_KEY}

View File

@@ -0,0 +1,12 @@
-- Table: public.annotations
-- DROP TABLE public.annotations;
CREATE TABLE IF NOT EXISTS public.annotations
(
id text NOT NULL,
annotations xml,
CONSTRAINT annotations_id_key UNIQUE (id)
);
GRANT ALL ON TABLE public.annotations TO current_user;

View File

@@ -0,0 +1,30 @@
-- Table: public.channel_videos
-- DROP TABLE public.channel_videos;
CREATE TABLE IF NOT EXISTS public.channel_videos
(
id text NOT NULL,
title text,
published timestamp with time zone,
updated timestamp with time zone,
ucid text,
author text,
length_seconds integer,
live_now boolean,
premiere_timestamp timestamp with time zone,
views bigint,
CONSTRAINT channel_videos_id_key UNIQUE (id)
);
GRANT ALL ON TABLE public.channel_videos TO current_user;
-- Index: public.channel_videos_ucid_idx
-- DROP INDEX public.channel_videos_ucid_idx;
CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx
ON public.channel_videos
USING btree
(ucid COLLATE pg_catalog."default");

View File

@@ -0,0 +1,25 @@
-- Table: public.channels
-- DROP TABLE public.channels;
CREATE TABLE IF NOT EXISTS public.channels
(
id text NOT NULL,
author text,
updated timestamp with time zone,
deleted boolean,
subscribed timestamp with time zone,
CONSTRAINT channels_id_key UNIQUE (id)
);
GRANT ALL ON TABLE public.channels TO current_user;
-- Index: public.channels_id_idx
-- DROP INDEX public.channels_id_idx;
CREATE INDEX IF NOT EXISTS channels_id_idx
ON public.channels
USING btree
(id COLLATE pg_catalog."default");

View File

@@ -0,0 +1,22 @@
-- Table: public.nonces
-- DROP TABLE public.nonces;
CREATE TABLE IF NOT EXISTS public.nonces
(
nonce text,
expire timestamp with time zone,
CONSTRAINT nonces_id_key UNIQUE (nonce)
);
GRANT ALL ON TABLE public.nonces TO current_user;
-- Index: public.nonces_nonce_idx
-- DROP INDEX public.nonces_nonce_idx;
CREATE INDEX IF NOT EXISTS nonces_nonce_idx
ON public.nonces
USING btree
(nonce COLLATE pg_catalog."default");

View File

@@ -0,0 +1,19 @@
-- Table: public.playlist_videos
-- DROP TABLE public.playlist_videos;
CREATE TABLE IF NOT EXISTS public.playlist_videos
(
title text,
id text,
author text,
ucid text,
length_seconds integer,
published timestamptz,
plid text references playlists(id),
index int8,
live_now boolean,
PRIMARY KEY (index,plid)
);
GRANT ALL ON TABLE public.playlist_videos TO current_user;

View File

@@ -0,0 +1,29 @@
-- Type: public.privacy
-- DROP TYPE public.privacy;
CREATE TYPE public.privacy AS ENUM
(
'Public',
'Unlisted',
'Private'
);
-- Table: public.playlists
-- DROP TABLE public.playlists;
CREATE TABLE IF NOT EXISTS public.playlists
(
title text,
id text primary key,
author text,
description text,
video_count integer,
created timestamptz,
updated timestamptz,
privacy privacy,
index int8[]
);
GRANT ALL ON public.playlists TO current_user;

View File

@@ -0,0 +1,23 @@
-- Table: public.session_ids
-- DROP TABLE public.session_ids;
CREATE TABLE IF NOT EXISTS public.session_ids
(
id text NOT NULL,
email text,
issued timestamp with time zone,
CONSTRAINT session_ids_pkey PRIMARY KEY (id)
);
GRANT ALL ON TABLE public.session_ids TO current_user;
-- Index: public.session_ids_id_idx
-- DROP INDEX public.session_ids_id_idx;
CREATE INDEX IF NOT EXISTS session_ids_id_idx
ON public.session_ids
USING btree
(id COLLATE pg_catalog."default");

View File

@@ -0,0 +1,29 @@
-- Table: public.users
-- DROP TABLE public.users;
CREATE TABLE IF NOT EXISTS public.users
(
updated timestamp with time zone,
notifications text[],
subscriptions text[],
email text NOT NULL,
preferences text,
password text,
token text,
watched text[],
feed_needs_update boolean,
CONSTRAINT users_email_key UNIQUE (email)
);
GRANT ALL ON TABLE public.users TO current_user;
-- Index: public.email_unique_idx
-- DROP INDEX public.email_unique_idx;
CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx
ON public.users
USING btree
(lower(email) COLLATE pg_catalog."default");

View File

@@ -0,0 +1,23 @@
-- Table: public.videos
-- DROP TABLE public.videos;
CREATE UNLOGGED TABLE IF NOT EXISTS public.videos
(
id text NOT NULL,
info text,
updated timestamp with time zone,
CONSTRAINT videos_pkey PRIMARY KEY (id)
);
GRANT ALL ON TABLE public.videos TO current_user;
-- Index: public.id_idx
-- DROP INDEX public.id_idx;
CREATE UNIQUE INDEX IF NOT EXISTS id_idx
ON public.videos
USING btree
(id COLLATE pg_catalog."default");

View File

@@ -3,8 +3,6 @@ services:
image: quay.io/invidious/invidious:latest
container_name: invidious
restart: unless-stopped
ports:
- "3001:3000"
environment:
INVIDIOUS_CONFIG: |
db:
@@ -14,45 +12,81 @@ services:
host: invidious-db
port: 5432
check_tables: true
signature_server: inv_sig_helper:12999
visitor_data: ${INVIDIOUS_VISITOR_DATA}
po_token: ${INVIDIOUS_PO_TOKEN}
invidious_companion:
- private_url: "http://invidious-companion:8282/companion"
public_url: "https://inv.${DOMAIN}"
invidious_companion_key: "${INVIDIOUS_COMPANION_KEY}"
external_port: 443
domain: inv.${DOMAIN}
https_only: true
hmac_key: "${INVIDIOUS_HMAC_KEY}"
hsts: false
log_level: Info
healthcheck:
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1
interval: 30s
timeout: 5s
retries: 2
logging:
options:
max-size: "1G"
max-file: "4"
depends_on:
- invidious-db
deploy:
resources:
limits:
cpus: '1'
memory: 2G
labels:
- "traefik.enable=true"
- "traefik.http.services.invidious.loadbalancer.server.port=3000"
- "traefik.http.routers.invidious.entrypoints=https"
- "traefik.http.routers.invidious.rule=Host(`invid.${DOMAIN}`, `i.${DOMAIN}`)"
- "traefik.http.routers.invidious.rule=Host(`inv.${DOMAIN}`)"
- "traefik.http.routers.invidious.tls.certresolver=http"
networks:
- web
- default
inv_sig_helper:
image: quay.io/invidious/inv-sig-helper:latest
container_name: inv-sig-helper
command: ["--tcp", "0.0.0.0:12999"]
environment:
- RUST_LOG=info
invidious-companion:
image: quay.io/invidious/invidious-companion:latest
container_name: invidious-companion
restart: unless-stopped
environment:
- SERVER_SECRET_KEY=${INVIDIOUS_COMPANION_KEY}
- SERVER_BASE_URL=https://inv.${DOMAIN}/companion
logging:
options:
max-size: "1G"
max-file: "4"
cap_drop:
- ALL
read_only: true
volumes:
- companion-cache:/var/tmp/youtubei.js:rw
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
cpus: '0.5'
memory: 1G
labels:
- "traefik.enable=true"
- "traefik.http.services.invidious-companion.loadbalancer.server.port=8282"
- "traefik.http.routers.invidious-companion.entrypoints=https"
- "traefik.http.routers.invidious-companion.rule=Host(`inv.${DOMAIN}`) && PathPrefix(`/companion`)"
- "traefik.http.routers.invidious-companion.tls.certresolver=http"
networks:
- web
- default
invidious-db:
image: postgres:14
container_name: invidious-db
restart: unless-stopped
volumes:
- ./postgres:/var/lib/postgresql/data
- postgres-data:/var/lib/postgresql/data
- ./config/sql:/config/sql
- ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh
environment:
@@ -61,6 +95,15 @@ services:
POSTGRES_PASSWORD: ${INVIDIOUS_DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
deploy:
resources:
limits:
cpus: '0.5'
memory: 1G
volumes:
postgres-data:
companion-cache:
networks:
web:

View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -eou pipefail
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channels.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/videos.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channel_videos.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/users.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/session_ids.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/nonces.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotations.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlists.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlist_videos.sql

View File

@@ -9,6 +9,8 @@ services:
- meshmonitor-data:/data
env_file:
- .env
labels:
- "com.ghost.tel/stack-type=dev-only"
volumes:
meshmonitor-data:
driver: local

View File

@@ -5,3 +5,5 @@ services:
restart: unless-stopped
ports:
- "8585:8080"
labels:
- "com.ghost.tel/stack-type=dev-only"

View File

@@ -12,3 +12,5 @@ services:
- /etc/ansible/hosts:/etc/ansible/hosts:ro
- ./.ssh:/var/core/mlLogWatcher/.ssh:ro
restart: unless-stopped
labels:
- "com.ghost.tel/stack-type=dev-only"

View File

@@ -8,6 +8,8 @@ services:
restart: always
env_file:
- .env
labels:
- "com.ghost.tel/stack-type=dev-only"
environment:
FRONTEND_PORT: "5001"
ports:

View File

@@ -18,3 +18,5 @@ services:
- /dev/dri:/dev/dri #optional
shm_size: "1gb"
restart: unless-stopped
labels:
- "com.ghost.tel/stack-type=dev-only"

View File

@@ -18,6 +18,7 @@ services:
depends_on:
- syncthing
labels:
- "com.ghost.tel/stack-type=dev-only"
- "traefik.enable=true"
- "traefik.docker.network=web"
- "traefik.http.routers.todo-obbytodo.entrypoints=https"

View File

@@ -19,6 +19,7 @@ services:
networks:
- web
labels:
- "com.ghost.tel/stack-type=dev-only"
- "traefik.enable=true"
- "traefik.http.routers.syncthing.entrypoints=https"
- "traefik.http.routers.syncthing.rule=Host(`syncthing.${DOMAIN}`)"

View File

@@ -8,6 +8,8 @@ services:
image: szurubooru/server:latest
depends_on:
- sql
labels:
- "com.ghost.tel/stack-type=dev-only"
environment:
## These should be the names of the dependent containers listed below,
## or FQDNs/IP addresses if these services are running outside of Docker

12
stacks/tlc/.env.template Normal file
View File

@@ -0,0 +1,12 @@
# Elasticsearch configuration
ELASTIC_URL=${ELASTIC_URL}
ELASTIC_INDEX=${ELASTIC_INDEX:-tlc-egre}
ELASTIC_API_KEY=${ELASTIC_API_KEY}
ELASTIC_VERIFY_CERTS=${ELASTIC_VERIFY_CERTS:-0}
# Qdrant configuration
QDRANT_URL=${QDRANT_URL}
QDRANT_COLLECTION=${QDRANT_COLLECTION:-tlc-captions-full}
QDRANT_VECTOR_NAME=${QDRANT_VECTOR_NAME:-}
QDRANT_VECTOR_SIZE=${QDRANT_VECTOR_SIZE:-1024}
QDRANT_EMBED_MODEL=${QDRANT_EMBED_MODEL:-BAAI/bge-large-en-v1.5}

View File

@@ -0,0 +1,74 @@
services:
# RSS Bridge - Converts YouTube channels to RSS feeds
rss-bridge:
image: rssbridge/rss-bridge:latest
container_name: tlc-rss-bridge
hostname: rss-bridge
restart: unless-stopped
labels:
- "com.ghost.tel/stack-type=dev-only"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
ports:
- "3001:80"
networks:
- web
# Feed Master - Aggregates multiple RSS feeds into unified feed
feed-master:
image: umputun/feed-master:latest
container_name: tlc-feed-master
hostname: feed-master
restart: unless-stopped
depends_on:
- rss-bridge
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
environment:
- DEBUG=false
- FM_DB=/srv/var/feed-master.bdb
- FM_CONF=/srv/etc/fm.yml
volumes:
- ./feed-master-config:/srv/etc
- ./feed-master-data:/srv/var
- ./feed-master-images:/srv/images
ports:
- "8097:8080"
networks:
- web
# TLC Search - Flask app for searching YouTube transcripts
search:
image: gitea.ghost.tel/knight/tlc-search:latest
container_name: tlc-search
hostname: tlc-search
restart: unless-stopped
depends_on:
- feed-master
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
env_file:
- .env
environment:
- RSS_FEED_URL=/rss/youtube-unified
- RSS_FEED_UPSTREAM=http://feed-master:8080
volumes:
- ./channels.yml:/app/python_app/channels.yml:ro
- ./data:/app/data:ro
ports:
- "8088:8080"
networks:
- web
networks:
web:
external: true

View File

@@ -0,0 +1,168 @@
# Feed Master Configuration
# Auto-generated from channels.yml
# Do not edit manually - regenerate using generate_feed_config_simple.py
feeds:
youtube-unified:
title: YouTube Unified Feed
description: Aggregated feed from all YouTube channels
link: https://youtube.com
language: "en-us"
sources:
- name: A Quality Existence
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC6vg0HkKKlgsWk-3HfV-vnw&format=Mrss
- name: Adams Fall
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCbD1Pm0TOcRK2zaCrwgcTTg&format=Mrss
- name: Andrea with the Bangs
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCeWWxwzgLYUbfjWowXhVdYw&format=Mrss
- name: Aphrael Pilotson
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCGHuURJ1XFHzPSeokf6510A&format=Mrss
- name: Cassidy van der Kamp
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCM9Z05vuQhMEwsV03u6DrLA&format=Mrss
- name: Channel UCCebR16tXbv
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCCebR16tXbv5Ykk9_WtCCug&format=Mrss
- name: Channel UCiJmdXTb76i
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCiJmdXTb76i8eIPXdJyf8ZQ&format=Mrss
- name: Charlie's Little Corner
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC952hDf_C4nYJdqwK7VzTxA&format=Mrss
- name: Chris Howard
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC704NVL2DyzYg3rMU9r1f7A&format=Mrss
- name: Christian Baxter
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCU5SNBfTo4umhjYz6M0Jsmg&format=Mrss
- name: Climbing Mt Sophia
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCk9O91WwruXmgu1NQrKZZEw&format=Mrss
- name: Corner Citizen
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCAXyF_HFeMgwS8nkGVeroAA&format=Mrss
- name: Davidbusuttil9086
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCxV18lwwh29DiWuooz7UCvg&format=Mrss
- name: Ein Sof - Infinite Reflections
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC4Rmxg7saTfwIpvq3QEzylQ&format=Mrss
- name: Emily Rajeh
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCn5mf-fcpBmkepIpZ8eFRng&format=Mrss
- name: Eric Seitz
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCTdH4nh6JTcfKUAWvmnPoIQ&format=Mrss
- name: Ethan Caughey
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCv2Qft5mZrmA9XAwnl9PU-g&format=Mrss
- name: faturechi
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCprytROeCztMOMe8plyJRMg&format=Mrss
- name: Finding Ideas
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC6Tvr9mBXNaAxLGRA_sUSRA&format=Mrss
- name: Free Rilian
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCT8Lq3ufaGEnCSS8WpFatqw&format=Mrss
- name: From Whom All Blessings Flow
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC_-YQbnPfBbIezMr1adZZiQ&format=Mrss
- name: Grahampardun
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCB0C8DEIQlQzvSGuGriBxtA&format=Mrss
- name: Grail Country
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCsi_x8c12NW9FR7LL01QXKA&format=Mrss
- name: Grey Hamilton
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCrZyTWGMdRM9_P26RKPvh3A&format=Mrss
- name: Grizwald Grim
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCAqTQ5yLHHH44XWwWXLkvHQ&format=Mrss
- name: Jesspurviance
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCQ7rVoApmYIpcmU7fB9RPyw&format=Mrss
- name: John Vervaeke
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCpqDUjTsof-kTNpnyWper_Q&format=Mrss
- name: Jonathan Dumeer
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC4GiA5Hnwy415uVRymxPK-w&format=Mrss
- name: Jonathan Pageau
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCtCTSf3UwRU14nYWr_xm-dQ&format=Mrss
- name: Jordan B Peterson
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCL_f53ZEJxp8TtlOkHwMV9Q&format=Mrss
- name: Jordan Hall
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCMzT-mdCqoyEv_-YZVtE7MQ&format=Mrss
- name: Joseph Lambrecht
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCiOZYvBGHw1Y6wyzffwEp9g&format=Mrss
- name: Justinsmorningcoffee
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCPUVeoQYyq8cndWwyczX6RA&format=Mrss
- name: Kale Zelden
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCiukuaNd_qzRDTW9qe2OC1w&format=Mrss
- name: Lancecleaver227
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCwF5LWNOFou_50bT65bq4Bg&format=Mrss
- name: Lucas Vos
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCez1fzMRGctojfis2lfRYug&format=Mrss
- name: Luke Thompson
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC_dnk5D4tFCRYCrKIcQlcfw&format=Mrss
- name: Marc Jackson
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCMJCtS8jKouJ2d8UIYzW3vg&format=Mrss
- name: Mark D Parker
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCVdSgEf9bLXFMBGSMhn7x4Q&format=Mrss
- name: Mark S
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC977g6oGYIJDQnsZOGjQBBA&format=Mrss
- name: Mary Kochan
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC2leFZRD0ZlQDQxpR2Zd8oA&format=Mrss
- name: Matthewparlato5626
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCosBhpwwGh_ueYq4ZSi5dGw&format=Mrss
- name: mcmosav
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCnojyPW0IgLWTQ0SaDQ1KBA&format=Mrss
- name: Michaelmartin8681
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCpLJJLVB_7v4Igq-9arja1A&format=Mrss
- name: More Christ
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCEPOn4cgvrrerg_-q_Ygw1A&format=Mrss
- name: Neal Daedalus
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC8mJqpS_EBbMcyuzZDF0TEw&format=Mrss
- name: Nechama Gluck
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC5goUoFM4LPim4eY4pwRXYw&format=Mrss
- name: OG Rose
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC6zHDj4D323xJkblnPTvY3Q&format=Mrss
- name: Paul Anleitner
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC2yCyOMUeem-cYwliC-tLJg&format=Mrss
- name: Paul Rene Nichols
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCDCfI162vhPvwdxW6X4nmiw&format=Mrss
- name: Paul VanderKlay
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCGsDIP_K6J6VSTqlq-9IPlg&format=Mrss
- name: President Foxman
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCMVG5eqpYFVEB-a9IqAOuHA&format=Mrss
- name: Rafekelley
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCDIPXp88qjAV3TiaR5Uo3iQ&format=Mrss
- name: Randos United
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCEzWTLDYmL8soRdQec9Fsjw&format=Mrss
- name: Randos United 2
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC1KgNsMdRoIA_njVmaDdHgA&format=Mrss
- name: Rebel Wisdom
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCFQ6Gptuq-sLflbJ4YY3Umw&format=Mrss
- name: Restoring Meaning
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCzX6R3ZLQh5Zma_5AsPcqPA&format=Mrss
- name: Rigel Windsong Thurston
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCWehDXDEdUpB58P7-Bg1cHg&format=Mrss
- name: Rightinchrist
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCZA5mUAyYcCL1kYgxbeMNrA&format=Mrss
- name: Ron Copperman
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC5yLuFQCms4nb9K2bGQLqIw&format=Mrss
- name: Sartori Studios
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC8SErJkYnDsYGh1HxoZkl-g&format=Mrss
- name: Secular Koranism
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCFLovlJ8RFApfjrf2y157xg&format=Mrss
- name: Shoulder Serf
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UChptV-kf8lnncGh7DA2m8Pw&format=Mrss
- name: Skankenstein
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCUSyTPWW4JaG1YfUPddw47Q&format=Mrss
- name: Strange Theology
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCEY1vGNBPsC3dCatZyK3Jkw&format=Mrss
- name: The Anadromist
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCIAtCuzdvgNJvSYILnHtdWA&format=Mrss
- name: The Chris Show
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UClIDP7_Kzv_7tDQjTv9EhrA&format=Mrss
- name: The Meaning Code
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCgp_r6WlBwDSJrP43Mz07GQ&format=Mrss
- name: the plebistocrat
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCaJ0CqiiMSTq4X0rycUOIjw&format=Mrss
- name: The Young Levite
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC1a4VtU_SMSfdRiwMJR33YQ&format=Mrss
- name: TheCommonToad
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC-QiBn6GsM3JZJAeAQpaGAA&format=Mrss
- name: TheScrollersPodcast
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC5uv-BxzCrN93B_5qbOdRWw&format=Mrss
- name: Transfigured
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCg7Ed0lecvko58ibuX1XHng&format=Mrss
- name: UpCycleClub
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCzw2FNI3IRphcAoVcUENOgQ&format=Mrss
- name: Wavesofobsession
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCedgru6YCto3zyXjlbuQuqA&format=Mrss
system:
update: 5m
max_per_feed: 5
max_total: 200
max_keep: 1000
base_url: http://localhost:8097