Asteroid Alert Article Notifier
Scrape a security blog and animate an asteroid across your desktop for new posts.
Introduction
30 Second Summary
You know that feeling of checking a website multiple times a day, hoping for new content, only to find nothing has changed? What if your computer could watch for you and send a dramatic visual alert the instant something new drops?
In this project, you will build a Python script that monitors a security news website and launches an animated asteroid across your screen whenever a new article appears.
What You'll Build
Asteroid Alert is a desktop notification tool that combines web scraping, GUI animation, and persistent state tracking into a single Python script.
By the end of this project, you'll have:
- A web scraper that fetches and parses hackingpassion.com for new article links using requests and BeautifulSoup.
- A transparent overlay animation that flies a high-definition asteroid image across your entire screen without interrupting your work.
- A JSON-based tracking system that remembers which articles you've already seen, so you only get notified about genuinely new posts.
- Secret Mission: Add a fiery particle trail that streams behind the asteroid as it flies across the screen.
Are there any prerequisites?
This project uses Windows-specific API calls for the transparent overlay, so you'll need a Windows computer with Python and VS Code already installed. No prior Python experience is required.
Before We Start
Before We Start...
Set Up Your Project
Before you can scrape websites or animate images, you need a workspace with the right tools installed.
In this step, you will create your project folder, install three Python packages, download an asteroid image, and scaffold your script file with all the imports and configuration it needs.
In this step, get ready to:
- Create your project folder and install the required Python packages.
- Download a high-definition asteroid PNG image for the animation.
- Create the script file with imports and configuration constants.
Create your project folder and install packages
Your script will rely on three external packages: one for making web requests, one for parsing HTML, and one for loading and resizing images.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir asteroid-alert
cd asteroid-alert
- Confirm the folder exists by running:
ls
You should see the asteroid-alert folder listed in the output.
- Open the folder in VS Code by running:
code .
A new VS Code window opens with the asteroid-alert folder loaded.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
- Install the three required packages by running this command:
pip install requests==2.34.2 beautifulsoup4==4.15.0 Pillow==12.3.0
What do these packages do?
- requests fetches web pages over HTTP. Your script uses it to download the hackingpassion.com homepage.
- beautifulsoup4 parses HTML into a searchable tree. Your script uses it to extract article links from the downloaded page.
- Pillow loads and resizes images. Your script uses it to prepare the asteroid PNG for the animation canvas.
- Verify each package installed correctly by running these commands one at a time:
pip show requests
pip show beautifulsoup4
pip show pillow
Each command prints metadata including a Name and Version field confirming the package is installed at the pinned version.
Seeing 'pip not found'?
Try using pip3 instead of pip in the install command. On some systems, pip is not on the PATH but pip3 is.
If neither works, make sure Python was added to your system PATH during installation.
Help me fix my pip installation.
Download an asteroid image
The animation displays a real asteroid image flying across your screen. You need a PNG file with a transparent background so only the asteroid is visible against your desktop.
- Open your web browser and search for "asteroid transparent background PNG" on a free PNG resource site such as pnghut.com, pngwing.com, or similar.
- Find an asteroid image that is at least 400x400 pixels.
- Download the image and save it as asteroid.png inside your asteroid-alert folder.
Why does the image need a transparent background?
The overlay window uses black as its transparent color. Any black pixels become invisible, letting your desktop show through. A PNG with a transparent background ensures only the asteroid itself is drawn on screen.
If the image has a solid background (white, blue, etc.), that background will appear as a visible rectangle flying across your screen instead of a clean asteroid shape.
- Confirm the file exists in your project by running:
ls
You should see asteroid.png listed in the output.
Don't see asteroid.png in the output?
Make sure you saved the file directly into the asteroid-alert folder on your Desktop, not into a Downloads folder or a subfolder.
Check that the filename is exactly asteroid.png with no extra characters like asteroid (1).png or asteroid.png.png.
Help me find where my downloaded image went.
Create the script with imports and configuration
Your entire project lives in a single Python file. The top of the file declares which libraries you need and defines constants that control the script's behavior.
- In VS Code, click File in the top menu bar.
- Click New File.
- Type asteroid_alert.py as the filename and press Enter.
- Add the import statements at the top of asteroid_alert.py by copying this code:
import tkinter as tk
from PIL import Image, ImageTk
import ctypes
import requests
from bs4 import BeautifulSoup
import json
import os
import time
What does each import do?
- tkinter creates the transparent overlay window and animation canvas.
- PIL (Pillow) loads and resizes the asteroid PNG for display in tkinter.
- ctypes calls Windows API functions to make the overlay click-through.
- requests fetches the hackingpassion.com homepage HTML.
- BeautifulSoup (from bs4) parses that HTML to find article links.
- json, os, and time handle file storage, path checks, and the polling interval.
- Below the imports, add the configuration constants by copying this code:
# Configuration
HACKINGPASSION_URL = "https://hackingpassion.com"
SEEN_ARTICLES_FILE = "seen_articles.json"
CHECK_INTERVAL = 300
ASTEROID_IMAGE = "asteroid.png"
ASTEROID_SIZE = (150, 150)
# Windows API constants
GWL_EXSTYLE = -20
WS_EX_LAYERED = 0x00080000
WS_EX_TRANSPARENT = 0x00000020
What do these constants control?
- HACKINGPASSION_URL is the site your script will scrape for new articles.
- SEEN_ARTICLES_FILE is the JSON file that remembers which articles have already triggered an alert.
- CHECK_INTERVAL is 300 seconds (5 minutes) between each scrape check.
- ASTEROID_SIZE sets the display dimensions for the flying asteroid image.
- The three Windows API constants (GWL_EXSTYLE, WS_EX_LAYERED, WS_EX_TRANSPARENT) are hex values that tell Windows to make the overlay window transparent and click-through.
- Save asteroid_alert.py by pressing Ctrl+S (Windows) or Cmd+S (macOS).
- Verify all imports resolve correctly by running this command in your terminal:
python -c "import requests; from bs4 import BeautifulSoup; from PIL import Image, ImageTk; print('All good!')"
You should see All good! printed with no errors.
Seeing a ModuleNotFoundError?
This means one of the packages did not install into the same Python environment your terminal is using. Run pip show requests to check if the package is installed. If it shows nothing, re-run the install command.
If you have multiple Python versions, try pip3 install or python3 -m pip install to target the correct version.
Help me fix my ModuleNotFoundError for requests, bs4, or PIL.
✔️ Awesome, I've got everything!
Great. Double check you have saved asteroid_alert.py and that asteroid.png is in the same folder.
ⓧ I'd like to double check the full code
Here is the complete asteroid_alert.py file at this point:
import tkinter as tk
from PIL import Image, ImageTk
import ctypes
import requests
from bs4 import BeautifulSoup
import json
import os
import time
# Configuration
HACKINGPASSION_URL = "https://hackingpassion.com"
SEEN_ARTICLES_FILE = "seen_articles.json"
CHECK_INTERVAL = 300
ASTEROID_IMAGE = "asteroid.png"
ASTEROID_SIZE = (150, 150)
# Windows API constants
GWL_EXSTYLE = -20
WS_EX_LAYERED = 0x00080000
WS_EX_TRANSPARENT = 0x00000020
Your workspace is ready with all dependencies installed and your script scaffolded. Next up, you will write the functions that scrape hackingpassion.com and detect new articles.
Build the Web Scraper
Your environment is ready with all the packages installed and your asteroid image waiting in the project folder.
Now you need a way to detect when new articles appear on hackingpassion.com. The site doesn't offer an RSS feed, so your script will fetch the homepage directly, parse out article links, and compare them against a local record of what it's already seen.
In this step, get ready to:
- Write persistence functions that save and load article data from a JSON file.
- Build a scraper function that fetches, parses, and filters new articles from the site.
- Test the scraper and confirm it detects articles from hackingpassion.com.
Write the JSON persistence functions
Your scraper needs to remember which articles it has already seen between runs. A simple JSON file works perfectly for this. Two small functions will handle reading from and writing to this file.
- In asteroid_alert.py, find the last line of your configuration constants (the WS_EX_TRANSPARENT line).
- Add the following two functions below your constants by copying this code:
def load_seen_articles():
# Check if the JSON file exists before trying to read it
if os.path.exists(SEEN_ARTICLES_FILE):
with open(SEEN_ARTICLES_FILE, "r") as f:
return json.load(f)
# Return an empty list on first run (no file yet)
return []
def save_seen_articles(articles):
# Write the full list of articles to JSON with readable formatting
with open(SEEN_ARTICLES_FILE, "w") as f:
json.dump(articles, f, indent=2)
What does this code do?
load_seen_articles() reads the JSON file and returns its contents as a Python list. If the file doesn't exist yet (first run), it returns an empty list instead of crashing.
save_seen_articles() writes the full article list to the JSON file with indent=2 for human-readable formatting.
- Save asteroid_alert.py.
- You won't see a change yet. The next function will use these to track articles.
Write the article scraping function
This is the core of your scraper. It fetches the hackingpassion.com homepage, pulls out all the article links, and compares them against the list of articles you've already seen.
- Below the save_seen_articles() function you just wrote, add the following function:
def check_for_new_articles():
try:
# Set a custom user-agent so the site knows what's requesting
headers = {"User-Agent": "AsteroidAlert/1.0"}
response = requests.get(HACKINGPASSION_URL, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
# Find all links on the page that look like article URLs
articles = []
for link in soup.find_all("a", href=True):
href = link["href"]
title = link.get_text(strip=True)
if (
href.startswith("https://hackingpassion.com/")
and len(title) > 30
and href != HACKINGPASSION_URL + "/"
and not href.endswith("#")
):
articles.append({"title": title[:100], "url": href})
# Compare found articles against previously seen ones
seen = load_seen_articles()
seen_urls = [a["url"] for a in seen]
new_articles = [a for a in articles if a["url"] not in seen_urls]
# Save newly found articles so we don't alert on them again
if new_articles:
all_articles = seen + new_articles
save_seen_articles(all_articles)
return new_articles
except requests.RequestException as e:
print(f"Error checking for articles: {e}")
return []
What does this code do?
- requests.get() fetches the homepage HTML. The timeout=10 prevents the script from hanging if the site is slow.
- BeautifulSoup parses the raw HTML and lets you search for specific elements. Here it finds all anchor tags (<a>) with an href attribute.
- The filtering logic keeps only links that start with the site URL, have a title longer than 30 characters (real articles, not navigation buttons), and aren't the homepage itself or hash links.
- The function compares found articles against previously seen URLs, saves any new ones to the JSON file, and returns only the new articles.
Why filter by title length?
A typical web page has dozens of links. Navigation menus, footer links, and social icons all use <a> tags too. Article titles are almost always longer than 30 characters, so this simple length check filters out the noise without needing to know the site's exact CSS class names.
- Save asteroid_alert.py.
- You won't see a visible change yet. The next substep tests this function.
Test the scraper
Time to see if your scraper actually works. You'll add a quick test line that calls your function and prints the results.
- At the very bottom of asteroid_alert.py, below the check_for_new_articles() function, add this test line:
print(check_for_new_articles())
- Save asteroid_alert.py.
- Run the script in your terminal by running this command:
python asteroid_alert.py
- You should see a list of dictionaries printed in your terminal, each containing a title and url key from hackingpassion.com articles.
- Check that a seen_articles.json file has appeared in your project folder by running:
ls
- You should see seen_articles.json listed alongside asteroid_alert.py and asteroid.png.
Seeing an empty list or an error?
- If you see [] (empty list), run the script a second time. On the first run all articles are new and get saved. On the second run they're all "seen" so the result is empty. Delete seen_articles.json and run again to see the full list.
- If you see a connection error, check that you have an active internet connection and that the site is reachable in your browser.
- If you see a NameError, make sure your print(check_for_new_articles()) line is below the function definition, not inside it.
Why isn't my scraper finding articles from hackingpassion.com?
Why did the second run return an empty list?
This is your persistence at work. The first run found articles and saved them to seen_articles.json. The second run compared the same articles against the saved list and found nothing new. That's exactly the behavior you want. The asteroid should only fire for genuinely new posts.
✔️ Awesome, I've got everything!
Great. Make sure you've saved asteroid_alert.py and that seen_articles.json exists in your project folder.
ⓧ I'd like to double check the full code
Here's what your complete asteroid_alert.py should look like at this point:
import tkinter as tk
from PIL import Image, ImageTk
import ctypes
import requests
from bs4 import BeautifulSoup
import json
import os
import time
# Configuration
HACKINGPASSION_URL = "https://hackingpassion.com"
SEEN_ARTICLES_FILE = "seen_articles.json"
CHECK_INTERVAL = 300
ASTEROID_IMAGE = "asteroid.png"
ASTEROID_SIZE = (150, 150)
# Windows API constants
GWL_EXSTYLE = -20
WS_EX_LAYERED = 0x00080000
WS_EX_TRANSPARENT = 0x00000020
def load_seen_articles():
if os.path.exists(SEEN_ARTICLES_FILE):
with open(SEEN_ARTICLES_FILE, "r") as f:
return json.load(f)
return []
def save_seen_articles(articles):
with open(SEEN_ARTICLES_FILE, "w") as f:
json.dump(articles, f, indent=2)
def check_for_new_articles():
try:
headers = {"User-Agent": "AsteroidAlert/1.0"}
response = requests.get(HACKINGPASSION_URL, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
articles = []
for link in soup.find_all("a", href=True):
href = link["href"]
title = link.get_text(strip=True)
if (
href.startswith("https://hackingpassion.com/")
and len(title) > 30
and href != HACKINGPASSION_URL + "/"
and not href.endswith("#")
):
articles.append({"title": title[:100], "url": href})
seen = load_seen_articles()
seen_urls = [a["url"] for a in seen]
new_articles = [a for a in articles if a["url"] not in seen_urls]
if new_articles:
all_articles = seen + new_articles
save_seen_articles(all_articles)
return new_articles
except requests.RequestException as e:
print(f"Error checking for articles: {e}")
return []
print(check_for_new_articles())
Your scraper is working and can detect new content on hackingpassion.com. Next up, you'll build the asteroid animation that fires when new articles are found.
Build the Asteroid Animation
Your scraper can detect new articles from hackingpassion.com. Now you need a way to announce them.
In this step, you will build a fullscreen transparent overlay that animates an asteroid image across your screen. The overlay uses the Windows API to make itself completely click-through, so it never interrupts your work.
In this step, get ready to:
- Write a function that makes a window click-through using Windows API calls.
- Build a fullscreen transparent overlay that loads and displays your asteroid image.
- Animate the asteroid across the screen at 60 FPS.
Write the click-through window function
Normal windows capture all mouse clicks, even transparent ones. To make our asteroid overlay truly invisible to interaction, we need to tell Windows to let clicks pass through the window entirely.
- In asteroid_alert.py, find the temporary test line at the very bottom of your file:
print(check_for_new_articles())
- Delete that entire line. You no longer need it for testing.
- Add the set_click_through() function at the bottom of your file (after the check_for_new_articles() function) by copying the following code:
def set_click_through(hwnd):
# Get current extended window styles
styles = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
# Add layered and transparent flags so clicks pass through
styles = styles | WS_EX_LAYERED | WS_EX_TRANSPARENT
ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, styles)
What does this code do?
This function takes a window handle (hwnd) and modifies its extended styles using the ctypes library to call Windows API functions directly.
The WS_EX_LAYERED flag enables transparency features on the window. The WS_EX_TRANSPARENT flag tells Windows to let all mouse events pass through to whatever is behind the window. Together, they create an overlay you can see but never accidentally click.
You will not see a change yet. The next function will use set_click_through() when creating the animation window.
Build the asteroid overlay window
The overlay needs to cover your entire screen, be completely transparent except for the asteroid, and stay on top of all other windows. This combines tkinter window management with the Pillow library for image loading.
- Add the show_asteroid() function below set_click_through() by copying this code:
def show_asteroid(article_title):
root = tk.Tk()
root.title("Asteroid Alert")
root.attributes("-topmost", True)
root.overrideredirect(True)
# Make the window cover the full screen
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
root.geometry(f"{screen_width}x{screen_height}+0+0")
# Set black as the transparent color (chroma key effect)
root.configure(bg="black")
root.attributes("-transparentcolor", "black")
# Create a full-screen canvas with black background
canvas = tk.Canvas(
root,
width=screen_width,
height=screen_height,
bg="black",
highlightthickness=0,
)
canvas.pack()
# Get the window handle and make it click-through
root.update_idletasks()
hwnd = ctypes.windll.user32.GetParent(root.winfo_id())
set_click_through(hwnd)
# Load and resize the asteroid image
img = Image.open(ASTEROID_IMAGE).convert("RGBA")
img = img.resize(ASTEROID_SIZE, Image.LANCZOS)
photo = ImageTk.PhotoImage(img)
# Position the asteroid off-screen to the right
y_position = screen_height // 3
start_x = screen_width + 100
# Place the asteroid image and article title on the canvas
asteroid = canvas.create_image(start_x, y_position, image=photo, anchor="center")
text = canvas.create_text(
start_x + 100,
y_position + 90,
text=article_title,
fill="white",
font=("Arial", 14, "bold"),
anchor="w",
)
speed = 6
What does this code do?
The function creates a borderless, fullscreen tkinter window positioned at the top-left corner. It sets the background to black, then tells Windows to treat all black pixels as transparent. This creates a chroma-key effect where only non-black elements (the asteroid image and white text) remain visible.
After obtaining the window handle with GetParent(root.winfo_id()), it applies the click-through styles. Then it loads your asteroid.png using Pillow, resizes it to 150x150, and places both the image and article title text off-screen to the right, ready to animate inward.
You will not see a change yet. The function is incomplete without its animation loop, which you will add next.
Add the animation loop
The asteroid needs to move smoothly from right to left across your screen. You will add an inner function that shifts the asteroid a few pixels every 16 milliseconds, creating roughly 60 frames per second of animation.
- Inside the show_asteroid() function, directly below the speed = 6 line, add the following code. Make sure it is indented one level to stay inside show_asteroid():
def animate():
# Move asteroid and text leftward
canvas.move(asteroid, -speed, 0)
canvas.move(text, -speed, 0)
# Check if asteroid has fully exited the left side
pos = canvas.coords(asteroid)
if pos[0] < -500:
root.destroy()
return
# Schedule the next frame (~60 FPS)
root.after(16, animate)
root.after(100, animate)
root.mainloop()
What does this code do?
The animate() function shifts both the asteroid image and text 6 pixels to the left on each frame. It checks the asteroid's x-coordinate, and once the asteroid passes 500 pixels beyond the left edge, it destroys the window to clean up.
The root.after(16, animate) call schedules the next frame in 16 milliseconds, giving approximately 60 frames per second. The initial root.after(100, animate) gives the window 100ms to fully render before the animation starts.
- Save asteroid_alert.py.
- Test the animation by temporarily adding this line at the very bottom of your file:
show_asteroid("Test Article Title")
- Save the file and run it by entering this command in your terminal:
python asteroid_alert.py
You should see your asteroid image fly smoothly across the screen from right to left, with "Test Article Title" in white text beside it. You can click through the animation to interact with windows behind it. The overlay disappears once the asteroid exits the left edge.
Don't see the asteroid?
- Make sure asteroid.png is in the same folder as asteroid_alert.py. The script looks for it in the current directory.
- Check that the indentation of animate(), root.after(100, animate), and root.mainloop() are all inside show_asteroid(). Python relies on indentation to determine function scope.
- If you see a brief flash but no animation, your image file may be corrupted. Try downloading a different asteroid PNG with a transparent background.
Help me debug my asteroid animation
- After confirming the animation works, delete the temporary show_asteroid("Test Article Title") line from the bottom of your file.
- Save asteroid_alert.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file with the test line removed.
ⓧ I'd like to double check the full code
Here is the complete asteroid_alert.py at this point:
import tkinter as tk
from PIL import Image, ImageTk
import ctypes
import requests
from bs4 import BeautifulSoup
import json
import os
import time
# Configuration
HACKINGPASSION_URL = "https://hackingpassion.com"
SEEN_ARTICLES_FILE = "seen_articles.json"
CHECK_INTERVAL = 300
ASTEROID_IMAGE = "asteroid.png"
ASTEROID_SIZE = (150, 150)
# Windows API constants
GWL_EXSTYLE = -20
WS_EX_LAYERED = 0x00080000
WS_EX_TRANSPARENT = 0x00000020
def load_seen_articles():
if os.path.exists(SEEN_ARTICLES_FILE):
with open(SEEN_ARTICLES_FILE, "r") as f:
return json.load(f)
return []
def save_seen_articles(articles):
with open(SEEN_ARTICLES_FILE, "w") as f:
json.dump(articles, f, indent=2)
def check_for_new_articles():
try:
headers = {"User-Agent": "AsteroidAlert/1.0"}
response = requests.get(HACKINGPASSION_URL, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
articles = []
for link in soup.find_all("a", href=True):
href = link["href"]
title = link.get_text(strip=True)
if (
href.startswith("https://hackingpassion.com/")
and len(title) > 30
and href != HACKINGPASSION_URL + "/"
and not href.endswith("#")
):
articles.append({"title": title[:100], "url": href})
seen = load_seen_articles()
seen_urls = [a["url"] for a in seen]
new_articles = [a for a in articles if a["url"] not in seen_urls]
if new_articles:
all_articles = seen + new_articles
save_seen_articles(all_articles)
return new_articles
except requests.RequestException as e:
print(f"Error checking for articles: {e}")
return []
def set_click_through(hwnd):
styles = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
styles = styles | WS_EX_LAYERED | WS_EX_TRANSPARENT
ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, styles)
def show_asteroid(article_title):
root = tk.Tk()
root.title("Asteroid Alert")
root.attributes("-topmost", True)
root.overrideredirect(True)
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
root.geometry(f"{screen_width}x{screen_height}+0+0")
root.configure(bg="black")
root.attributes("-transparentcolor", "black")
canvas = tk.Canvas(
root,
width=screen_width,
height=screen_height,
bg="black",
highlightthickness=0,
)
canvas.pack()
root.update_idletasks()
hwnd = ctypes.windll.user32.GetParent(root.winfo_id())
set_click_through(hwnd)
img = Image.open(ASTEROID_IMAGE).convert("RGBA")
img = img.resize(ASTEROID_SIZE, Image.LANCZOS)
photo = ImageTk.PhotoImage(img)
y_position = screen_height // 3
start_x = screen_width + 100
asteroid = canvas.create_image(start_x, y_position, image=photo, anchor="center")
text = canvas.create_text(
start_x + 100,
y_position + 90,
text=article_title,
fill="white",
font=("Arial", 14, "bold"),
anchor="w",
)
speed = 6
def animate():
canvas.move(asteroid, -speed, 0)
canvas.move(text, -speed, 0)
pos = canvas.coords(asteroid)
if pos[0] < -500:
root.destroy()
return
root.after(16, animate)
root.after(100, animate)
root.mainloop()
Your asteroid animation is working. Next, you will wire everything together into a continuously running monitor that checks for new articles and launches the asteroid automatically.
Connect Everything Into a Monitor
Your asteroid animation flies smoothly across the screen. Now it's time to wire the scraper and animation together into a single continuously running monitor.
The monitor needs to handle a tricky edge case: on first launch, it should save all existing articles as "already seen" without flooding your screen with asteroids. After that, it polls every 5 minutes and fires an asteroid only for genuinely new posts.
In this step, get ready to:
- Write the run_monitor() function with first-run logic and a polling loop.
- Add the script entry point so it runs when you execute the file.
- Test the full monitor and verify asteroid triggers.
Write the run_monitor() function
The monitor function orchestrates everything your script does. It detects whether this is a first run or a subsequent run, prints status messages, and enters an infinite loop that checks for new articles on a timer.
- In asteroid_alert.py, scroll to the bottom of the file (below the show_asteroid() function).
- Add the run_monitor() function by pasting the following code:
def run_monitor():
print("=== Asteroid Alert ===")
print(f"Monitoring {HACKINGPASSION_URL} for new articles...")
print(f"Checking every {CHECK_INTERVAL // 60} minutes.")
print("Press Ctrl+C to stop.\n")
# Detect if this is the first time running the script
first_run = not os.path.exists(SEEN_ARTICLES_FILE)
if first_run:
# First run: save all current articles without firing asteroids
print("First run: saving current articles as already seen...")
articles = check_for_new_articles()
print(f"Saved {len(articles)} existing articles. Will alert on new ones.\n")
else:
# Subsequent run: check for new articles and fire asteroids
new_articles = check_for_new_articles()
if new_articles:
print(f"Found {len(new_articles)} new article(s)!")
for article in new_articles:
print(f" NEW: {article['title']}")
show_asteroid(article["title"])
else:
print("No new articles found.")
# Enter the polling loop - check every 5 minutes
while True:
time.sleep(CHECK_INTERVAL)
print("Checking for new articles...")
new_articles = check_for_new_articles()
if new_articles:
print(f"Found {len(new_articles)} new article(s)!")
for article in new_articles:
print(f" NEW: {article['title']}")
show_asteroid(article["title"])
else:
print("No new articles.")
What does this code do?
This function has three distinct phases:
- Startup banner prints a greeting and configuration info so you know the monitor is alive.
- First-run detection checks whether seen_articles.json exists. If it does not, the script saves all current articles as "seen" without launching any asteroids. This prevents a flood of animations on first use.
- Polling loop sleeps for 5 minutes (CHECK_INTERVAL = 300 seconds), then checks again. For each new article found, it calls show_asteroid() to trigger the animation.
Why save everything on first run?
Without first-run logic, launching the script for the first time would treat every article on the homepage as "new" and fire dozens of asteroids back-to-back. By silently cataloging existing articles on the first run, the monitor only alerts you to genuinely new posts going forward.
Add the script entry point
Python uses a special guard to distinguish between a file being run directly and a file being imported by another script. This ensures your monitor only starts when you deliberately execute the file.
- Add the following code at the very bottom of asteroid_alert.py, below the run_monitor() function:
if __name__ == "__main__":
run_monitor()
What does this code do?
When Python runs a file directly, it sets the special variable __name__ to "__main__". This guard makes run_monitor() execute only when you run the script yourself, not when it gets imported as a module.
- Save asteroid_alert.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before running it in the next substep.
ⓧ I'd like to double check the full code
import tkinter as tk
from PIL import Image, ImageTk
import ctypes
import requests
from bs4 import BeautifulSoup
import json
import os
import time
# Configuration
HACKINGPASSION_URL = "https://hackingpassion.com"
SEEN_ARTICLES_FILE = "seen_articles.json"
CHECK_INTERVAL = 300
ASTEROID_IMAGE = "asteroid.png"
ASTEROID_SIZE = (150, 150)
# Windows API constants
GWL_EXSTYLE = -20
WS_EX_LAYERED = 0x00080000
WS_EX_TRANSPARENT = 0x00000020
def load_seen_articles():
if os.path.exists(SEEN_ARTICLES_FILE):
with open(SEEN_ARTICLES_FILE, "r") as f:
return json.load(f)
return []
def save_seen_articles(articles):
with open(SEEN_ARTICLES_FILE, "w") as f:
json.dump(articles, f, indent=2)
def check_for_new_articles():
try:
headers = {"User-Agent": "AsteroidAlert/1.0"}
response = requests.get(HACKINGPASSION_URL, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
articles = []
for link in soup.find_all("a", href=True):
href = link["href"]
title = link.get_text(strip=True)
if (
href.startswith("https://hackingpassion.com/")
and len(title) > 30
and href != HACKINGPASSION_URL + "/"
and not href.endswith("#")
):
articles.append({"title": title[:100], "url": href})
seen = load_seen_articles()
seen_urls = [a["url"] for a in seen]
new_articles = [a for a in articles if a["url"] not in seen_urls]
if new_articles:
all_articles = seen + new_articles
save_seen_articles(all_articles)
return new_articles
except requests.RequestException as e:
print(f"Error checking for articles: {e}")
return []
def set_click_through(hwnd):
styles = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
styles = styles | WS_EX_LAYERED | WS_EX_TRANSPARENT
ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, styles)
def show_asteroid(article_title):
root = tk.Tk()
root.title("Asteroid Alert")
root.attributes("-topmost", True)
root.overrideredirect(True)
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
root.geometry(f"{screen_width}x{screen_height}+0+0")
root.configure(bg="black")
root.attributes("-transparentcolor", "black")
canvas = tk.Canvas(
root,
width=screen_width,
height=screen_height,
bg="black",
highlightthickness=0,
)
canvas.pack()
root.update_idletasks()
hwnd = ctypes.windll.user32.GetParent(root.winfo_id())
set_click_through(hwnd)
img = Image.open(ASTEROID_IMAGE).convert("RGBA")
img = img.resize(ASTEROID_SIZE, Image.LANCZOS)
photo = ImageTk.PhotoImage(img)
y_position = screen_height // 3
start_x = screen_width + 100
asteroid = canvas.create_image(start_x, y_position, image=photo, anchor="center")
text = canvas.create_text(
start_x + 100,
y_position + 90,
text=article_title,
fill="white",
font=("Arial", 14, "bold"),
anchor="w",
)
speed = 6
def animate():
canvas.move(asteroid, -speed, 0)
canvas.move(text, -speed, 0)
pos = canvas.coords(asteroid)
if pos[0] < -500:
root.destroy()
return
root.after(16, animate)
root.after(100, animate)
root.mainloop()
def run_monitor():
print("=== Asteroid Alert ===")
print(f"Monitoring {HACKINGPASSION_URL} for new articles...")
print(f"Checking every {CHECK_INTERVAL // 60} minutes.")
print("Press Ctrl+C to stop.\n")
first_run = not os.path.exists(SEEN_ARTICLES_FILE)
if first_run:
print("First run: saving current articles as already seen...")
articles = check_for_new_articles()
print(f"Saved {len(articles)} existing articles. Will alert on new ones.\n")
else:
new_articles = check_for_new_articles()
if new_articles:
print(f"Found {len(new_articles)} new article(s)!")
for article in new_articles:
print(f" NEW: {article['title']}")
show_asteroid(article["title"])
else:
print("No new articles found.")
while True:
time.sleep(CHECK_INTERVAL)
print("Checking for new articles...")
new_articles = check_for_new_articles()
if new_articles:
print(f"Found {len(new_articles)} new article(s)!")
for article in new_articles:
print(f" NEW: {article['title']}")
show_asteroid(article["title"])
else:
print("No new articles.")
if __name__ == "__main__":
run_monitor()
Run the full monitor
Time to see everything working together. You will run the script, watch it handle the first-run case, and then test the asteroid trigger by resetting the seen articles file.
- First, delete your existing seen_articles.json file so you can observe the first-run behavior. Run this in your terminal:
del seen_articles.json
- Start the monitor by running:
python asteroid_alert.py
You should see the startup banner followed by the first-run message. The script will report how many existing articles it saved, then enter the polling loop.
- Stop the script by pressing Ctrl+C in the terminal.
- Now test the asteroid trigger. Delete seen_articles.json again and run the script a second time:
del seen_articles.json
python asteroid_alert.py
Wait, won't it just save them again?
Yes, on first run it saves all articles silently. To actually see asteroids fly, you need to simulate a "new article" scenario. The easiest way: run the script once (first-run saves articles), stop it, delete seen_articles.json, then edit the file to contain only an empty list [] and run again. This tricks the script into thinking every article is new.
- To trigger the asteroid animations, create a seen_articles.json file containing just an empty list by running:
echo [] > seen_articles.json
- Run the script again:
python asteroid_alert.py
This time you should see asteroids flying across your screen for every article on the site. Each asteroid carries the article title alongside it.
- Press Ctrl+C to stop the script when you are done watching.
Script exits immediately or no asteroids appear?
- Make sure seen_articles.json contains exactly [] (an empty JSON array). If the file does not exist at all, the script treats it as a first run and saves without animating.
- Check that asteroid.png is in the same folder as your script. A missing image will throw a FileNotFoundError.
- If you see a network error, confirm you have an internet connection. The scraper needs to reach hackingpassion.com.
Help me debug why my asteroid alert script isn't triggering animations.
Secret mission
Add a Fiery Particle Trail
Your asteroid flies across the screen, but it looks a bit lonely. Real asteroids entering an atmosphere leave a blazing trail of fire behind them. Can you add a randomized particle system that streams fiery orange, red, and yellow sparks behind the asteroid as it moves?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- asteroid_alert.py script file (your main Python program).
- seen_articles.json file (stores previously detected article URLs).
- asteroid.png image file (the asteroid graphic used in the animation).
- Python packages installed via pip: requests, beautifulsoup4, Pillow.
Keep everything running
No action needed. Choose this if you want to keep monitoring hackingpassion.com for new articles.
- Run python asteroid_alert.py in a terminal any time you want to start monitoring.
- The script checks for new articles every 5 minutes and fires an asteroid animation when one appears.
- To set it up to auto-start with Windows, add a shortcut to your Startup folder.
- Stop the script any time with Ctrl+C in the terminal.
Pause - I'll come back to this later
Stop the running script to free up your terminal. Your project files stay intact for next time.
- Press Ctrl+C in the terminal where the script is running to stop it.
- Your seen_articles.json file is preserved, so the script remembers which articles it already detected.
- When you come back, just run python asteroid_alert.py again to resume monitoring.
Delete - I don't want to use this again
Remove all project files and uninstall the packages to start completely fresh.
- Press Ctrl+C in the terminal to stop the script if it is still running.
- Delete your project folder by running this command (macOS/Linux):
cd ~/Desktop
rm -rf asteroid-alert
On Windows, run this instead:
cd ~\Desktop
rmdir /s /q asteroid-alert
- Uninstall the Python packages by running:
pip uninstall requests beautifulsoup4 Pillow -y
What about Python and VS Code?
Python and VS Code are general-purpose tools you will likely use for other projects. There is no need to uninstall them unless you are certain you will not use them again.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional desktop notification system that scrapes a live website and launches a dramatic asteroid animation across your screen whenever new content appears.
You've learned how to:
- Built a web scraper that fetches and parses hackingpassion.com using requests and BeautifulSoup, filtering article links from navigation noise.
- Created a transparent, click-through desktop overlay using tkinter and Windows API calls via ctypes, animating a high-definition asteroid PNG at 60 FPS.
- Implemented persistent article tracking with a JSON file so the monitor only fires notifications for genuinely new posts, polling every 5 minutes.
- Secret Mission: Added a randomized fiery particle trail that streams behind the asteroid for a dramatic visual effect.
Ready to quiz yourself?