Discord Bot That Announces Your Twitch Streams and YouTube Videos
If you stream or upload videos, what brings the most people is your Discord finding out right away. This bot checks every two minutes whether you're live on Twitch or have uploaded something to YouTube and announces it in the channel you choose, mentioning the role you want. It's about 120 lines of Python and doesn't need any YouTube key.
How it works
Every two minutes the bot asks the Twitch API which channels on your list are live and reads the public feed of each YouTube channel (the latest 15 videos, with no key or quota). What it has already announced it notes in a file, announced.json, so it doesn't repeat the alert if it restarts.
Why every two minutes and not instantly? Twitch has instant notifications (EventSub), but via webhook they need a web address with HTTPS that Twitch can call, and via WebSocket they require a user token you have to keep renewing. Polling every few minutes works anywhere, with no ports to open, and for a stream alert a two-minute delay isn't noticeable.
If you haven't made a bot yet, start with your first bot in Python: this one builds on the same skeleton.
Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.
1. The keys you need
The Discord bot
As with any bot: create the application at discord.com/developers/applications, get the token in “Bot” and invite it with the “bot” permission. In the alerts channel give it “View Channel”, “Send Messages” and “Embed Links” (for the stream card). If you're going to mention @everyone or a role that can't be mentioned, also “Mention @everyone, @here, and All Roles”.
The Twitch application
At dev.twitch.tv/console, “Register Your Application”. Twitch requires two-factor authentication to be on for your account. Give it a name, http://localhost as the OAuth redirect URL, a category and client type “Confidential”. When you save it, it gives you the Client ID; click “New Secret” for the Client Secret. With both, the bot requests its own app token (and another one when it expires).
The YouTube channel ID
It starts with UC and has 24 characters. On the channel page, open the description (“…more”), scroll down to “Share channel” and click “Copy channel ID”. If the channel address is youtube.com/channel/UC…, you already have it there.
The Discord IDs
Turn on “User Settings” → “Advanced” → “Developer Mode”. Then right-click the alerts channel → “Copy Channel ID”; and, to mention a role, in “Server Settings” → “Roles”, right-click the role → “Copy Role ID”. The mention is written <@&ID>.
2. The code
Three files in one folder. The .env holds the keys and the lists (comma-separated, no spaces or quotes): TWITCH_USERS is the channel name as it appears in twitch.tv/name; if you only want Twitch or only YouTube, leave the other list empty. MENTION can be a role's <@&ID>, @everyone or nothing.
The first time, for YouTube it only notes the videos that were already there (it doesn't announce old ones); for Twitch, if you're already live when you start it, it announces once. Each stream has its own number, so if the bot restarts midway it doesn't announce again.
discord.py
aiohttp
python-dotenvDISCORD_TOKEN=paste-the-bot-token-here
ALERTS_CHANNEL=123456789012345678
MENTION=<@&123456789012345678>
TWITCH_CLIENT_ID=your-client-id
TWITCH_CLIENT_SECRET=your-client-secret
TWITCH_USERS=your_channel,your_friends_channel
YOUTUBE_CHANNELS=UCxxxxxxxxxxxxxxxxxxxxxximport json
import os
import xml.etree.ElementTree as ET
import aiohttp
import discord
from discord.ext import tasks
from dotenv import load_dotenv
load_dotenv() # reads the .env
ALERTS_CHANNEL = int(os.environ["ALERTS_CHANNEL"])
TWITCH_ID = os.environ.get("TWITCH_CLIENT_ID", "")
TWITCH_SECRET = os.environ.get("TWITCH_CLIENT_SECRET", "")
TWITCH = [u.strip().lower() for u in os.environ.get("TWITCH_USERS", "").split(",") if u.strip()]
YOUTUBE = [c.strip() for c in os.environ.get("YOUTUBE_CHANNELS", "").split(",") if c.strip()]
MENTION = os.environ.get("MENTION", "") # <@&ROLE_ID>, @everyone or nothing
FILE = "announced.json" # what was already announced, so restarts don't repeat it
NS = {"a": "http://www.w3.org/2005/Atom", "yt": "http://www.youtube.com/xml/schemas/2015"}
client = discord.Client(intents=discord.Intents.default())
twitch_token = None
try:
with open(FILE) as f:
announced = json.load(f)
except FileNotFoundError:
announced = {"twitch": {}, "youtube": {}}
async def get_token(session):
global twitch_token
data = {"client_id": TWITCH_ID, "client_secret": TWITCH_SECRET, "grant_type": "client_credentials"}
async with session.post("https://id.twitch.tv/oauth2/token", params=data) as r:
r.raise_for_status()
twitch_token = (await r.json())["access_token"]
async def live_streams(session):
"""The TWITCH_USERS channels that are live right now."""
if not TWITCH:
return []
if twitch_token is None:
await get_token(session)
for _ in range(2):
headers = {"Client-Id": TWITCH_ID, "Authorization": f"Bearer {twitch_token}"}
query = [("user_login", u) for u in TWITCH]
async with session.get("https://api.twitch.tv/helix/streams", params=query, headers=headers) as r:
if r.status == 401: # the token expires: get another and retry
await get_token(session)
continue
r.raise_for_status()
return (await r.json())["data"]
return []
async def videos(session, channel):
"""The latest videos of a YouTube channel, newest first."""
url = "https://www.youtube.com/feeds/videos.xml"
async with session.get(url, params={"channel_id": channel}) as r:
r.raise_for_status()
root = ET.fromstring(await r.text())
return [
{
"id": e.find("yt:videoId", NS).text,
"title": e.find("a:title", NS).text,
"author": e.find("a:author/a:name", NS).text,
"link": e.find("a:link", NS).get("href"),
}
for e in root.findall("a:entry", NS)
]
@tasks.loop(minutes=2)
async def watch():
channel = client.get_channel(ALERTS_CHANNEL) or await client.fetch_channel(ALERTS_CHANNEL)
async with aiohttp.ClientSession() as session:
try:
for s in await live_streams(session):
if announced["twitch"].get(s["user_login"]) == s["id"]:
continue # this stream was already announced
announced["twitch"][s["user_login"]] = s["id"]
card = discord.Embed(
title=s["title"] or "Live",
url=f"https://www.twitch.tv/{s['user_login']}",
description=f"Playing {s['game_name']}" if s["game_name"] else None,
color=0x9146FF,
)
card.set_image(url=s["thumbnail_url"].replace("{width}", "1280").replace("{height}", "720"))
await channel.send(f"{MENTION} 🔴 **{s['user_name']}** is live".strip(), embed=card)
except Exception as error: # Twitch isn't answering: try again next round
print("Twitch:", error)
for c in YOUTUBE:
try:
items = await videos(session, c)
except Exception as error: # the feed sometimes fails: next round
print("YouTube:", c, error)
continue
seen = announced["youtube"].get(c)
announced["youtube"][c] = [v["id"] for v in items]
if seen is None:
continue # the first time it only notes the existing ones
for v in reversed(items):
if v["id"] not in seen:
await channel.send(f"{MENTION} 📺 **{v['author']}** uploaded “{v['title']}”\n{v['link']}".strip())
with open(FILE, "w") as f:
json.dump(announced, f)
@client.event
async def on_ready():
print(f"Logged in as {client.user}")
if not watch.is_running():
watch.start()
client.run(os.environ["DISCORD_TOKEN"])3. Try it
With Python 3.10 or newer: “pip install -r requirements.txt” and “python app.py”. When it says “Logged in as…”, upload a test video (public: the feed doesn't include unlisted or private ones) or go live, and the alert appears within two or three minutes. If it doesn't, look at what the console prints: “Twitch: 401” or “400” is almost always a badly copied Client ID or secret; “YouTube: 404”, a wrong channel ID (or a temporary feed glitch, which fixes itself on the next round).
Want to change the alert text or the card? They're in the two channel.send lines: the color, title and image are changed in discord.Embed.
4. Keep it online
An alerts bot is only useful if it's online when you go live: on your computer it wouldn't announce anything once you switch it off. It needs to be hosted on a server.
On NapNodes: create the bot, switch it to Python in “Settings” → “Language”, upload the three files in “Files” and click “Start”. The libraries in requirements.txt install themselves at startup and app.py is the startup file that comes set. announced.json stays in “Files” between restarts. If you'd rather bring the code from GitHub, don't push the .env to the repository (upload it separately in “Files”) and put announced.json in the .gitignore. The whole process is in the Discord bot hosting guide.
It uses very little: the NapNodes free bot, with 256 MB, is more than enough. The catch is that the free one has to be renewed with one click every 7 days (we email you the day before); if you forget, it goes offline and stops announcing until you start it. For a bot that can't fail on stream day, the Mini plan (512 MB) stays online with nothing to renew (Discord bot pricing).
Frequently asked questions
How do I make my Discord announce when I'm live on Twitch?
With a bot that asks the Twitch API every few minutes whether you're live and, when you go live, sends a message to the channel you choose. You need the Discord bot token and a Twitch application (Client ID and Client Secret, free at dev.twitch.tv/console).
Do I need a YouTube API key to announce new videos?
No. Every channel has a public feed (youtube.com/feeds/videos.xml?channel_id=…) with its latest 15 videos, and the bot reads it with no key or quota.
How often does the bot check?
It checks every two minutes (you change it in @tasks.loop). Going below one minute doesn't add much and makes more requests.
Why doesn't it mention @everyone?
The bot needs the “Mention @everyone, @here, and All Roles” permission in that channel. Without it, the message is sent, but the mention doesn't notify anyone.
Where do I keep the bot so it always announces?
On a bot host. On NapNodes the free 256 MB bot is more than enough (renewed with one click every week); the Mini plan stays online with nothing to renew (free options, with their downsides).
Shall we set it up for you?
Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.
Keep reading
- How to Make an AI Discord Bot (ChatGPT, Gemini…) in Python
Make a Discord bot that answers with AI: an /ask command in Python that uses ChatGPT, Gemini or another AI, with its key kept safe and your credit protected.
- Discord Bot Showing Your Minecraft Server Status (Players Online)
Build a Discord bot in Python that shows whether your Minecraft server is online, how many are playing and who, with /status. Full code, free.
- Discord Role Bot: Buttons So Everyone Picks Their Own Roles (Python and JavaScript)
Make your own Discord self-role bot in Python or JavaScript: a message with buttons to add and remove roles, no database and no reaction roles needed.