Crime Drama Awards Tracker
Query two movie APIs to build a styled HTML report of top crime dramas.
Introduction
30 Second Summary
Every streaming platform uses data from multiple sources to build those "Top 10" lists you see on the homepage. Behind the scenes, someone combined ratings, award histories, and studio information from different databases into one view.
In this project, you will build a Python script that pulls movie data from two different APIs and combines them into a unified dataset. The script generates a styled HTML report showcasing the best crime drama films from the past 5 years.
What You'll Build
A Python script that discovers top-rated crime dramas using the TMDB API, enriches each film with award and director information from the OMDb API, and outputs a styled HTML report with movie posters, ratings, and award badges.
By the end of this project, you'll have:
- A working Python script that queries two movie APIs and combines their data into a unified dataset.
- A styled HTML report displaying top-rated crime drama films with posters, directors, studios, and award information.
- Practical experience parsing JSON responses and enriching data from multiple REST API sources.
- Secret Mission: Add interactive sorting and filtering controls to your HTML report so users can sort by rating or filter to show only award-winning films.
Are there any prerequisites?
This project assumes basic Python knowledge (variables, loops, functions, and dictionaries) and general familiarity with what an API is. You do not need prior experience with TMDB, OMDb, or HTML generation.
Set Up Your Project Environment
This project pulls movie data from two different APIs and combines them into one polished report. Before you can write any code, you need a clean Python environment and access credentials for both data sources.
In this step, you will create a project folder, set up a virtual environment, install the requests library, and grab free API keys from TMDB and OMDb.
In this step, get ready to:
- Create a Python virtual environment and install the requests library.
- Sign up for a free TMDB API key.
- Sign up for a free OMDb API key.
Create your project folder and install dependencies
A virtual environment keeps your project's packages isolated from the rest of your system. This means the libraries you install here will not interfere with other Python projects on your machine.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal (macOS) or PowerShell (Windows), then pressing Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir crime-drama-tracker
cd crime-drama-tracker
- Create and activate a Python virtual environment by running the commands for your operating system:
macOS
- Create the virtual environment and activate it by running:
python3 -m venv venv
source venv/bin/activate
You should see (venv) appear at the start of your terminal prompt.
Windows
- Create the virtual environment and activate it by running:
python -m venv venv
venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt.
What is a virtual environment?
A virtual environment is an isolated Python installation inside your project folder. Any packages you install with pip while the environment is active go into the venv folder rather than your system-wide Python.
This isolation means you can install specific package versions without conflicts.
- Install the requests library by running:
pip install requests==2.34.2
What does this command do?
pip install requests==2.34.2 downloads and installs version 2.34.2 of the requests library. This is the HTTP library your script will use to call both the TMDB and OMDb APIs.
- Verify the installation by running:
python3 -c "import requests; print(requests.__version__)"
You should see 2.34.2 printed in your terminal.
Seeing an error or wrong version?
- Make sure your virtual environment is active. Look for (venv) at the start of your prompt.
- On Windows, use python instead of python3 in the verify command.
- If pip is not found, try pip3 instead.
Why isn't my requests library installing correctly?
- Open the crime-drama-tracker folder in VS Code by running:
code .
Keep your terminal open
You will run your Python script from this same terminal later. Keep the terminal open with the virtual environment active for the rest of the project.
Sign up for a free TMDB API key
TMDB (The Movie Database) is the first API your script will call. It has a powerful discovery endpoint that lets you filter movies by genre, rating, and release date.
- Open https://www.themoviedb.org/ in your browser.
- Click Join TMDB in the top-right corner and create a free account.
- Confirm your email address by clicking the verification link TMDB sends you.
- Log in, then click your profile avatar in the top-right corner.
- Click Settings.
- Click API in the left sidebar.
- Click the link to request an API key.
- Select Developer as the type of use.
- Accept the terms of use, then fill in the required fields with a short description of your personal project.
- After submitting, TMDB displays your credentials on the API page.
- Find the field labelled API Read Access Token and copy the long token string.
- Paste it somewhere safe (a text file or notes app) for use in Step 2.
Why the Bearer token instead of the API key?
TMDB offers two authentication methods. The API Read Access Token (Bearer token) is the recommended default because it works across both API v3 and v4 with a single header.
Your script will send this token in an Authorization header with every request.
Don't see the API link in your settings?
- Make sure you are logged in and have verified your email address.
- Use a desktop browser. TMDB notes that the API registration process is not optimized for mobile devices.
Help me find the API settings in my TMDB account.
Sign up for a free OMDb API key
OMDb (Open Movie Database) is the second API your script will call. It provides director names and award details that TMDB does not include.
- Open https://www.omdbapi.com/apikey.aspx in your browser.
- Under Account Type, select FREE! (1,000 daily limit).
- Enter your email address in the Email field.
- Enter your name in the Name fields.
- In the Use field, type a short description like Personal learning project.
- Click Submit.
- Check your email inbox for a message from OMDb containing your API key.
- Copy the API key and paste it in the same safe location as your TMDB token.
How long does the email take?
The OMDb key usually arrives within a few minutes. If you use a Yahoo or Microsoft email (Hotmail, Outlook, Live), delivery may be slower. Check your spam folder if it does not appear within 15 minutes.
Still haven't received your key?
- Check your spam or junk folder.
- Try again with a different email provider (Gmail works reliably).
- If the key does not arrive within an hour, use the contact link on the OMDb site to reach out to the developer directly.
Help me get my OMDb API key.
Your environment is ready and both API keys are in hand. Next up, you will write the function that discovers top-rated crime drama films from TMDB.
Discover Top Crime Drama Films with TMDB
Your environment is set up and ready to go. Now it's time to start building the actual script that pulls movie data from the TMDB API.
TMDB's Discover endpoint lets you filter movies by genre, rating, and release date. You'll use it to find the best-rated crime dramas from the past 5 years, then look up which studio produced each one.
In this step, get ready to:
- Create the Python script file with API configuration.
- Write a function that discovers top crime drama films from TMDB.
- Write a function that looks up the production studio for each film.
Create the script file and add configuration
Before writing any logic, you need a central place to store your API credentials and base URLs. This keeps your code organized and makes it easy to update tokens later.
- In VS Code, create a new file called crime_drama_tracker.py inside your crime-drama-tracker folder.
- Add the following imports and configuration to the file:
import requests
import time
import webbrowser
import os
# --- Configuration ---
TMDB_ACCESS_TOKEN = "your-tmdb-access-token-here"
OMDB_API_KEY = "your-omdb-api-key-here"
TMDB_BASE_URL = "https://api.themoviedb.org/3"
TMDB_IMAGE_BASE = "https://image.tmdb.org/t/p/w500"
OMDB_BASE_URL = "http://www.omdbapi.com/"
HEADERS = {
"Authorization": f"Bearer {TMDB_ACCESS_TOKEN}",
"accept": "application/json"
}
What does this code do?
TMDB_ACCESS_TOKEN and OMDB_API_KEY hold your personal API credentials. Replace the placeholder strings with the real values you got in Step 1.
TMDB_BASE_URL and OMDB_BASE_URL are the root URLs for each API. TMDB_IMAGE_BASE is the prefix TMDB uses for poster image URLs.
The HEADERS dictionary sends your Bearer token with every TMDB request. This is how TMDB authenticates you.
- Replace your-tmdb-access-token-here with the API Read Access Token you copied from TMDB.
- Replace your-omdb-api-key-here with the OMDb API key from your email.
- Save crime_drama_tracker.py.
You won't see a change yet. The next chunk uses these constants to call the TMDB API.
Write the TMDB discover function
TMDB's Discover endpoint lets you search for movies matching specific criteria. You'll filter for films that are both Crime (genre ID 80) and Drama (genre ID 18), released since 2021, with at least 500 votes to ensure meaningful ratings.
- Add the following function below the HEADERS dictionary in crime_drama_tracker.py:
# --- Step 2: Discover Crime Drama Films ---
def discover_crime_dramas():
"""Find top-rated crime drama films from the past 5 years using TMDB."""
url = f"{TMDB_BASE_URL}/discover/movie"
params = {
"with_genres": "80,18",
"sort_by": "vote_average.desc",
"vote_count.gte": 500,
"primary_release_date.gte": "2021-01-01",
"primary_release_date.lte": "2026-12-31",
"language": "en-US",
"page": 1
}
response = requests.get(url, headers=HEADERS, params=params)
response.raise_for_status()
data = response.json()
movies = []
for movie in data["results"]:
movies.append({
"id": movie["id"],
"title": movie["title"],
"release_date": movie["release_date"],
"year": movie["release_date"][:4] if movie["release_date"] else "N/A",
"rating": movie["vote_average"],
"poster_url": f"{TMDB_IMAGE_BASE}{movie['poster_path']}" if movie["poster_path"] else "",
})
print(f"Found {len(movies)} crime drama films from TMDB.")
return movies
What does this code do?
The params dictionary defines your search filters. "with_genres": "80,18" uses comma-separated IDs to require BOTH Crime AND Drama (AND logic). "vote_count.gte": 500 filters out obscure films with too few ratings.
The loop extracts key fields from each movie in the response. It builds the full poster URL by combining the TMDB image base URL with each movie's poster_path. It also slices the first 4 characters of release_date to get just the year.
The function returns a list of dictionaries. Each dictionary represents one movie with its ID, title, year, rating, and poster URL.
- Save crime_drama_tracker.py.
- Test the function by running this command in your terminal:
python3 -c "from crime_drama_tracker import discover_crime_dramas; discover_crime_dramas()"
You should see output like Found 20 crime drama films from TMDB. in your terminal. The exact number may vary depending on current TMDB data.
Seeing an authentication error?
- Double-check that you pasted your TMDB API Read Access Token (the long Bearer token), not the shorter API Key.
- Make sure the token string has no leading or trailing spaces.
- Confirm your virtual environment is still activated (you should see (venv) at the start of your terminal prompt).
Help me debug my TMDB API authentication error.
Write the studio lookup function
The Discover endpoint doesn't include production company information. To find out which studio made each film, you need a second API call to TMDB's movie details endpoint.
- Add the following function below discover_crime_dramas() in crime_drama_tracker.py:
def get_studio(movie_id):
"""Get the primary production company for a movie from TMDB."""
url = f"{TMDB_BASE_URL}/movie/{movie_id}"
response = requests.get(url, headers=HEADERS)
response.raise_for_status()
data = response.json()
companies = data.get("production_companies", [])
if companies:
return companies[0]["name"]
return "Unknown Studio"
What does this code do?
This function calls TMDB's movie details endpoint (/3/movie/{movie_id}) which returns full information about a single film, including its production_companies array.
Each production company object has fields like id, name, and origin_country. The function grabs the first company's name as the primary studio. If no companies are listed, it returns "Unknown Studio" as a fallback.
- Save crime_drama_tracker.py.
You won't see a change yet. In the next step, you'll call this function for each discovered movie to build a complete dataset.
Why a separate function for studios?
TMDB's Discover endpoint is optimized for searching across many movies at once, so it only returns lightweight data (title, rating, poster). Detailed information like production companies requires a per-movie request to the details endpoint.
This is a common pattern when working with REST APIs. You make a broad query first, then enrich each result with targeted follow-up calls.
✔️ Awesome, I've got everything!
Great. Double check you've saved crime_drama_tracker.py and move on to the next step.
ⓧ I'd like to double check the full code
Here's the complete crime_drama_tracker.py file up to this point:
import requests
import time
import webbrowser
import os
# --- Configuration ---
TMDB_ACCESS_TOKEN = "your-tmdb-access-token-here"
OMDB_API_KEY = "your-omdb-api-key-here"
TMDB_BASE_URL = "https://api.themoviedb.org/3"
TMDB_IMAGE_BASE = "https://image.tmdb.org/t/p/w500"
OMDB_BASE_URL = "http://www.omdbapi.com/"
HEADERS = {
"Authorization": f"Bearer {TMDB_ACCESS_TOKEN}",
"accept": "application/json"
}
# --- Step 2: Discover Crime Drama Films ---
def discover_crime_dramas():
"""Find top-rated crime drama films from the past 5 years using TMDB."""
url = f"{TMDB_BASE_URL}/discover/movie"
params = {
"with_genres": "80,18",
"sort_by": "vote_average.desc",
"vote_count.gte": 500,
"primary_release_date.gte": "2021-01-01",
"primary_release_date.lte": "2026-12-31",
"language": "en-US",
"page": 1
}
response = requests.get(url, headers=HEADERS, params=params)
response.raise_for_status()
data = response.json()
movies = []
for movie in data["results"]:
movies.append({
"id": movie["id"],
"title": movie["title"],
"release_date": movie["release_date"],
"year": movie["release_date"][:4] if movie["release_date"] else "N/A",
"rating": movie["vote_average"],
"poster_url": f"{TMDB_IMAGE_BASE}{movie['poster_path']}" if movie["poster_path"] else "",
})
print(f"Found {len(movies)} crime drama films from TMDB.")
return movies
def get_studio(movie_id):
"""Get the primary production company for a movie from TMDB."""
url = f"{TMDB_BASE_URL}/movie/{movie_id}"
response = requests.get(url, headers=HEADERS)
response.raise_for_status()
data = response.json()
companies = data.get("production_companies", [])
if companies:
return companies[0]["name"]
return "Unknown Studio"
Your script can now find the top crime dramas and look up their studios. Next up, you'll enrich each film with director and award information from the OMDb API.
Enrich Movies with Award Data from OMDb
In the previous step, you tapped into TMDB's discover endpoint to pull a list of top-rated crime dramas. You now have titles, ratings, and poster URLs for each film.
TMDB is excellent for discovery, but it does not include award history or director information in its responses. The OMDb API fills that gap. In this step, you will query OMDb for each film, parse its unstructured awards text, and merge everything into a single enriched dataset.
In this step, get ready to:
- Write a function that queries OMDb for director and award details.
- Parse unstructured award strings into clean categories.
- Combine TMDB and OMDb data into enriched movie records.
Query the OMDb API for each movie
The OMDb API accepts a movie title and year, then returns detailed metadata including the director's name and an awards summary string.
- In crime_drama_tracker.py, scroll to the bottom of the file (below the get_studio function).
- Add the OMDb query function by copying the following code:
# --- Step 3: Enrich with OMDb Data ---
def get_omdb_data(title, year):
"""Get director and awards info from OMDb API."""
params = {
"apikey": OMDB_API_KEY,
"t": title,
"y": year,
"type": "movie"
}
response = requests.get(OMDB_BASE_URL, params=params)
response.raise_for_status()
data = response.json()
if data.get("Response") == "True":
return {
"director": data.get("Director", "N/A"),
"awards": data.get("Awards", "N/A")
}
return {"director": "N/A", "awards": "N/A"}
What does this code do?
- The params dictionary builds the OMDb query using t for the title, y for the year, and type to restrict results to movies only.
- OMDb returns a Response field set to "True" when a match is found. If no match exists, the function returns safe defaults of "N/A".
- The function extracts only the Director and Awards fields from the response, ignoring everything else.
- Save crime_drama_tracker.py.
- You won't see a change yet. The next function will use get_omdb_data to build enriched movie records.
Seeing a syntax error on save?
Make sure you pasted the function below get_studio with a blank line separating the two functions. Python requires consistent indentation throughout each function body.
Help me fix a syntax error in my OMDb function
Parse the awards string into categories
OMDb returns awards as a plain text string like "Won 4 Oscars. 157 wins & 220 nominations total" or "N/A" when no data exists. You need to parse this unstructured text into clean categories your report can use.
- Below the get_omdb_data function, add the awards categorization function:
def categorize_awards(awards_string):
"""Parse the OMDb Awards string into a category."""
if not awards_string or awards_string == "N/A":
return "No Awards"
if "Won" in awards_string and "Oscar" in awards_string:
return "Oscar Winner"
if "Won" in awards_string or "win" in awards_string.lower():
return "Award Winner"
if "Nominated" in awards_string:
return "Nominated"
return "No Awards"
What does this code do?
This function checks the awards string in priority order. It looks for Oscar wins first (the highest distinction), then any other wins, then nominations. If nothing matches, it defaults to "No Awards".
- "Won" + "Oscar" together means the film won an Academy Award.
- "Won" alone or "win" (case-insensitive) catches other major awards like Golden Globes or BAFTA wins.
- "Nominated" catches films that received nominations but did not win.
- Save crime_drama_tracker.py.
- Test the function by running this quick check in your terminal:
python3 -c "from crime_drama_tracker import categorize_awards; print(categorize_awards('Won 4 Oscars. 157 wins & 220 nominations total')); print(categorize_awards('N/A'))"
- You should see Oscar Winner on the first line and No Awards on the second line.
Getting an ImportError?
Make sure your terminal is in the crime-drama-tracker folder (the same folder that contains crime_drama_tracker.py). Also confirm your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
Help me fix my categorize_awards import error
Combine TMDB and OMDb data into enriched records
Now you have all the building blocks. The final function in this step loops through each movie, calls both APIs, and merges everything into one enriched dictionary per film.
- Below the categorize_awards function, add the enrichment function:
def enrich_movies(movies):
"""Add studio, director, and awards data to each movie."""
enriched = []
for i, movie in enumerate(movies):
print(f" Enriching {i + 1}/{len(movies)}: {movie['title']}...")
# Get studio from TMDB
studio = get_studio(movie["id"])
time.sleep(0.25)
# Get director and awards from OMDb
omdb_data = get_omdb_data(movie["title"], movie["year"])
time.sleep(0.25)
movie["studio"] = studio
movie["director"] = omdb_data["director"]
movie["awards_raw"] = omdb_data["awards"]
movie["award_category"] = categorize_awards(omdb_data["awards"])
enriched.append(movie)
award_winners = sum(1 for m in enriched if m["award_category"] != "No Awards")
print(f"\nEnrichment complete: {len(enriched)} films, {award_winners} award winners.")
return enriched
What does this code do?
- The loop calls get_studio (TMDB) and get_omdb_data (OMDb) for each movie, adding four new keys: studio, director, awards_raw, and award_category.
- time.sleep(0.25) pauses briefly between API calls. This avoids overwhelming either API with rapid requests.
- At the end, it counts how many films have awards and prints a summary so you can confirm enrichment worked.
Why call both APIs per movie?
TMDB is great at discovery (finding films by genre and rating) but lacks award history. OMDb includes awards and director info but has no discover or filter mechanism. Combining both gives you a complete picture that neither API provides alone.
- Save crime_drama_tracker.py.
- You won't see output yet. In the next step, you will write the main() function that calls enrich_movies and generates the final HTML report.
Seeing a NameError for get_studio or get_omdb_data?
Make sure enrich_movies is defined below both get_studio and get_omdb_data in your file. Python reads top to bottom, so functions must be defined before they are called.
Help me fix a NameError in my enrich_movies function
✔️ Awesome, I've got everything!
Great. Double check you have saved crime_drama_tracker.py with all three new functions.
ⓧ I'd like to double check the full code
import requests
import time
import webbrowser
import os
# --- Configuration ---
TMDB_ACCESS_TOKEN = "your-tmdb-access-token-here"
OMDB_API_KEY = "your-omdb-api-key-here"
TMDB_BASE_URL = "https://api.themoviedb.org/3"
TMDB_IMAGE_BASE = "https://image.tmdb.org/t/p/w500"
OMDB_BASE_URL = "http://www.omdbapi.com/"
HEADERS = {
"Authorization": f"Bearer {TMDB_ACCESS_TOKEN}",
"accept": "application/json"
}
# --- Step 2: Discover Crime Drama Films ---
def discover_crime_dramas():
"""Find top-rated crime drama films from the past 5 years using TMDB."""
url = f"{TMDB_BASE_URL}/discover/movie"
params = {
"with_genres": "80,18",
"sort_by": "vote_average.desc",
"vote_count.gte": 500,
"primary_release_date.gte": "2021-01-01",
"primary_release_date.lte": "2026-12-31",
"language": "en-US",
"page": 1
}
response = requests.get(url, headers=HEADERS, params=params)
response.raise_for_status()
data = response.json()
movies = []
for movie in data["results"]:
movies.append({
"id": movie["id"],
"title": movie["title"],
"release_date": movie["release_date"],
"year": movie["release_date"][:4] if movie["release_date"] else "N/A",
"rating": movie["vote_average"],
"poster_url": f"{TMDB_IMAGE_BASE}{movie['poster_path']}" if movie["poster_path"] else "",
})
print(f"Found {len(movies)} crime drama films from TMDB.")
return movies
def get_studio(movie_id):
"""Get the primary production company for a movie from TMDB."""
url = f"{TMDB_BASE_URL}/movie/{movie_id}"
response = requests.get(url, headers=HEADERS)
response.raise_for_status()
data = response.json()
companies = data.get("production_companies", [])
if companies:
return companies[0]["name"]
return "Unknown Studio"
# --- Step 3: Enrich with OMDb Data ---
def get_omdb_data(title, year):
"""Get director and awards info from OMDb API."""
params = {
"apikey": OMDB_API_KEY,
"t": title,
"y": year,
"type": "movie"
}
response = requests.get(OMDB_BASE_URL, params=params)
response.raise_for_status()
data = response.json()
if data.get("Response") == "True":
return {
"director": data.get("Director", "N/A"),
"awards": data.get("Awards", "N/A")
}
return {"director": "N/A", "awards": "N/A"}
def categorize_awards(awards_string):
"""Parse the OMDb Awards string into a category."""
if not awards_string or awards_string == "N/A":
return "No Awards"
if "Won" in awards_string and "Oscar" in awards_string:
return "Oscar Winner"
if "Won" in awards_string or "win" in awards_string.lower():
return "Award Winner"
if "Nominated" in awards_string:
return "Nominated"
return "No Awards"
def enrich_movies(movies):
"""Add studio, director, and awards data to each movie."""
enriched = []
for i, movie in enumerate(movies):
print(f" Enriching {i + 1}/{len(movies)}: {movie['title']}...")
# Get studio from TMDB
studio = get_studio(movie["id"])
time.sleep(0.25)
# Get director and awards from OMDb
omdb_data = get_omdb_data(movie["title"], movie["year"])
time.sleep(0.25)
movie["studio"] = studio
movie["director"] = omdb_data["director"]
movie["awards_raw"] = omdb_data["awards"]
movie["award_category"] = categorize_awards(omdb_data["awards"])
enriched.append(movie)
award_winners = sum(1 for m in enriched if m["award_category"] != "No Awards")
print(f"\nEnrichment complete: {len(enriched)} films, {award_winners} award winners.")
return enriched
Your enrichment pipeline is ready. Each movie now carries studio, director, and award data from two separate sources. Next up, you will turn this enriched data into a polished HTML report with styled movie cards and award badges.
Generate a Styled HTML Report
Your script can now discover top crime dramas from TMDB and enrich each film with director and award data from OMDb.
All that data is sitting in a list of dictionaries, but it is not very exciting to look at in a terminal. In this step, you will turn your enriched movie data into a polished, dark-themed HTML report that opens directly in your browser.
In this step, get ready to:
- Write a function that generates a styled HTML report from your movie data.
- Create movie cards with poster images, ratings, and award badges.
- Add a main function that runs the full pipeline and opens the report in your browser.
Build movie cards with award badges
Each movie in your enriched list has a title, year, rating, poster URL, studio, director, and award category. You need to loop through this list and build an HTML card for each film, including a coloured badge to indicate its award status.
- In crime_drama_tracker.py, scroll to the bottom of the file (below the enrich_movies function).
- Add the start of your generate_html_report function by copying the following code:
# --- Step 4: Generate HTML Report ---
def generate_html_report(movies):
"""Generate a styled HTML report from the enriched movie data."""
award_winners = sum(1 for m in movies if m["award_category"] != "No Awards")
oscar_winners = sum(1 for m in movies if m["award_category"] == "Oscar Winner")
movie_cards = ""
for movie in movies:
badge_class = ""
badge_text = ""
if movie["award_category"] == "Oscar Winner":
badge_class = "badge-oscar"
badge_text = "Oscar Winner"
elif movie["award_category"] == "Award Winner":
badge_class = "badge-award"
badge_text = "Award Winner"
elif movie["award_category"] == "Nominated":
badge_class = "badge-nominated"
badge_text = "Nominated"
badge_html = f'<span class="badge {badge_class}">{badge_text}</span>' if badge_text else ""
movie_cards += f"""
<div class="movie-card" data-rating="{movie['rating']}" data-year="{movie['year']}" data-category="{movie['award_category']}">
<img src="{movie['poster_url']}" alt="{movie['title']} poster" class="poster">
<div class="movie-info">
<h2>{movie['title']} <span class="year">({movie['year']})</span></h2>
<div class="rating">TMDB Rating: {movie['rating']}/10</div>
<div class="detail"><strong>Director:</strong> {movie['director']}</div>
<div class="detail"><strong>Studio:</strong> {movie['studio']}</div>
<div class="detail"><strong>Awards:</strong> {movie['awards_raw']}</div>
{badge_html}
</div>
</div>
"""
What does this code do?
This code starts the report-generation function and loops through every enriched movie to build an HTML card.
- The two sum() lines count how many films won any award and how many specifically won Oscars. These numbers appear in the report header later.
- The for loop checks each film's award_category and assigns a CSS class (badge-oscar, badge-award, or badge-nominated) to colour the badge appropriately.
- Each card includes data-rating, data-year, and data-category attributes on the div. These are standard HTML data attributes that store values for potential future use.
- Save crime_drama_tracker.py.
You won't see a change yet. The next chunk builds the HTML page template that wraps around these cards.
Create the HTML template with embedded CSS
Now you need the HTML skeleton that wraps around your movie cards. This includes a dark colour scheme, a header with summary statistics, and CSS styling for the card layout and award badges.
- Directly below the card loop you just added (still inside the generate_html_report function), add the HTML template:
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crime Drama Awards Tracker</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #1a1a2e;
color: #eee;
padding: 2rem;
}}
.header {{
text-align: center;
margin-bottom: 2rem;
padding: 2rem;
background: linear-gradient(135deg, #16213e, #0f3460);
border-radius: 12px;
}}
.header h1 {{
font-size: 2.2rem;
margin-bottom: 0.5rem;
color: #e94560;
}}
.stats {{
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 1rem;
}}
.stat {{
text-align: center;
}}
.stat-number {{
font-size: 2rem;
font-weight: bold;
color: #e94560;
}}
.stat-label {{
font-size: 0.9rem;
color: #aaa;
}}
.movie-grid {{
display: grid;
gap: 1.5rem;
}}
.movie-card {{
display: flex;
background: #16213e;
border-radius: 12px;
overflow: hidden;
transition: transform 0.2s;
}}
.movie-card:hover {{
transform: translateY(-4px);
}}
.poster {{
width: 150px;
min-height: 225px;
object-fit: cover;
flex-shrink: 0;
}}
.movie-info {{
padding: 1.2rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}}
.movie-info h2 {{
font-size: 1.3rem;
color: #fff;
}}
.year {{
color: #aaa;
font-weight: normal;
font-size: 1rem;
}}
.rating {{
color: #e94560;
font-weight: bold;
font-size: 1.1rem;
}}
.detail {{
color: #ccc;
font-size: 0.95rem;
}}
.badge {{
display: inline-block;
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
margin-top: 0.5rem;
width: fit-content;
}}
.badge-oscar {{
background: linear-gradient(135deg, #f5af19, #f12711);
color: #fff;
}}
.badge-award {{
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
}}
.badge-nominated {{
background: linear-gradient(135deg, #4facfe, #00f2fe);
color: #1a1a2e;
}}
</style>
</head>
<body>
<div class="header">
<h1>Crime Drama Awards Tracker</h1>
<p>Top-rated crime drama films from the past 5 years</p>
<div class="stats">
<div class="stat">
<div class="stat-number">{len(movies)}</div>
<div class="stat-label">Films Found</div>
</div>
<div class="stat">
<div class="stat-number">{award_winners}</div>
<div class="stat-label">Award Winners</div>
</div>
<div class="stat">
<div class="stat-number">{oscar_winners}</div>
<div class="stat-label">Oscar Winners</div>
</div>
</div>
</div>
<div class="movie-grid">
{movie_cards}
</div>
</body>
</html>"""
What does this template do?
This is one large Python f-string that builds the entire HTML page. The double curly braces ({{ and }}) are how you include literal curly braces inside an f-string without Python treating them as variable placeholders.
- The <style> block defines a dark-themed colour palette with #1a1a2e as the background. Card styling uses flexbox for layout.
- The header shows three stats pulled from the Python variables: total films, award winners, and Oscar winners.
- The {movie_cards} placeholder inserts all the card HTML you built in the previous chunk.
- Award badges use gradient backgrounds. Gold for Oscars, purple for other awards, and blue for nominations.
- Save crime_drama_tracker.py.
You won't see a change yet. The next chunk adds the code that writes this HTML to a file and opens it in your browser.
Save the report and run the full script
The last piece of generate_html_report writes the HTML string to a file. Then you need a main() function that calls all three pipeline stages in order and opens the finished report.
- Directly below the html = f"""...""" assignment (still inside generate_html_report), add the file-writing code:
filename = "crime_drama_report.html"
with open(filename, "w", encoding="utf-8") as f:
f.write(html)
print(f"\nReport saved to {filename}")
return filename
What does this code do?
This writes the full HTML string to a file called crime_drama_report.html in your project folder. The encoding="utf-8" ensures special characters in movie titles display correctly. The function returns the filename so main() can open it.
- Now add the main() function below generate_html_report (outside the function, at the top indentation level):
# --- Main ---
def main():
print("=" * 50)
print(" Crime Drama Awards Tracker")
print("=" * 50)
print("\n[1/3] Discovering top crime drama films...")
movies = discover_crime_dramas()
print("\n[2/3] Enriching with studio, director & award data...")
enriched_movies = enrich_movies(movies)
print("\n[3/3] Generating HTML report...")
filename = generate_html_report(enriched_movies)
filepath = os.path.abspath(filename)
webbrowser.open(f"file://{filepath}")
print(f"Report opened in your browser!")
if __name__ == "__main__":
main()
What does this code do?
- The main() function runs your three-stage pipeline in order: discover films, enrich them, and generate the report.
- Progress messages like [1/3] help you see which stage is running since enrichment takes a few seconds per film.
- The os.path.abspath() call converts the filename to a full file path. The webbrowser.open() call opens that path in your default browser.
- The if __name__ == "__main__": guard ensures main() only runs when you execute the script directly, not when importing it.
- Save crime_drama_tracker.py.
- Run the full script by entering this command in your terminal:
python3 crime_drama_tracker.py
You should see progress messages in the terminal as the script discovers films, enriches each one, and generates the report. After a few seconds, crime_drama_report.html opens automatically in your browser.
The report shows a dark-themed page with a red header displaying stats, followed by a grid of movie cards. Each card has a poster image on the left and film details on the right, with coloured award badges.
Report not opening or showing errors?
- If you see IndentationError, make sure all code inside generate_html_report is indented by 4 spaces. The html = f"""...""" line, the filename line, and the return line should all be at the same indentation level.
- If the browser opens but shows a blank page, check that the closing triple quotes """ for the HTML f-string are on their own line with no extra spaces before them.
- If you see an API error, confirm your TMDB and OMDb API keys are correctly pasted in the configuration section at the top of the file.
Help me debug my Crime Drama Awards Tracker script
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete crime_drama_tracker.py file for comparison:
import requests
import time
import webbrowser
import os
# --- Configuration ---
TMDB_ACCESS_TOKEN = "your-tmdb-access-token-here"
OMDB_API_KEY = "your-omdb-api-key-here"
TMDB_BASE_URL = "https://api.themoviedb.org/3"
TMDB_IMAGE_BASE = "https://image.tmdb.org/t/p/w500"
OMDB_BASE_URL = "http://www.omdbapi.com/"
HEADERS = {
"Authorization": f"Bearer {TMDB_ACCESS_TOKEN}",
"accept": "application/json"
}
# --- Step 2: Discover Crime Drama Films ---
def discover_crime_dramas():
"""Find top-rated crime drama films from the past 5 years using TMDB."""
url = f"{TMDB_BASE_URL}/discover/movie"
params = {
"with_genres": "80,18",
"sort_by": "vote_average.desc",
"vote_count.gte": 500,
"primary_release_date.gte": "2021-01-01",
"primary_release_date.lte": "2026-12-31",
"language": "en-US",
"page": 1
}
response = requests.get(url, headers=HEADERS, params=params)
response.raise_for_status()
data = response.json()
movies = []
for movie in data["results"]:
movies.append({
"id": movie["id"],
"title": movie["title"],
"release_date": movie["release_date"],
"year": movie["release_date"][:4] if movie["release_date"] else "N/A",
"rating": movie["vote_average"],
"poster_url": f"{TMDB_IMAGE_BASE}{movie['poster_path']}" if movie["poster_path"] else "",
})
print(f"Found {len(movies)} crime drama films from TMDB.")
return movies
def get_studio(movie_id):
"""Get the primary production company for a movie from TMDB."""
url = f"{TMDB_BASE_URL}/movie/{movie_id}"
response = requests.get(url, headers=HEADERS)
response.raise_for_status()
data = response.json()
companies = data.get("production_companies", [])
if companies:
return companies[0]["name"]
return "Unknown Studio"
# --- Step 3: Enrich with OMDb Data ---
def get_omdb_data(title, year):
"""Get director and awards info from OMDb API."""
params = {
"apikey": OMDB_API_KEY,
"t": title,
"y": year,
"type": "movie"
}
response = requests.get(OMDB_BASE_URL, params=params)
response.raise_for_status()
data = response.json()
if data.get("Response") == "True":
return {
"director": data.get("Director", "N/A"),
"awards": data.get("Awards", "N/A")
}
return {"director": "N/A", "awards": "N/A"}
def categorize_awards(awards_string):
"""Parse the OMDb Awards string into a category."""
if not awards_string or awards_string == "N/A":
return "No Awards"
if "Won" in awards_string and "Oscar" in awards_string:
return "Oscar Winner"
if "Won" in awards_string or "win" in awards_string.lower():
return "Award Winner"
if "Nominated" in awards_string:
return "Nominated"
return "No Awards"
def enrich_movies(movies):
"""Add studio, director, and awards data to each movie."""
enriched = []
for i, movie in enumerate(movies):
print(f" Enriching {i + 1}/{len(movies)}: {movie['title']}...")
# Get studio from TMDB
studio = get_studio(movie["id"])
time.sleep(0.25)
# Get director and awards from OMDb
omdb_data = get_omdb_data(movie["title"], movie["year"])
time.sleep(0.25)
movie["studio"] = studio
movie["director"] = omdb_data["director"]
movie["awards_raw"] = omdb_data["awards"]
movie["award_category"] = categorize_awards(omdb_data["awards"])
enriched.append(movie)
award_winners = sum(1 for m in enriched if m["award_category"] != "No Awards")
print(f"\nEnrichment complete: {len(enriched)} films, {award_winners} award winners.")
return enriched
# --- Step 4: Generate HTML Report ---
def generate_html_report(movies):
"""Generate a styled HTML report from the enriched movie data."""
award_winners = sum(1 for m in movies if m["award_category"] != "No Awards")
oscar_winners = sum(1 for m in movies if m["award_category"] == "Oscar Winner")
movie_cards = ""
for movie in movies:
badge_class = ""
badge_text = ""
if movie["award_category"] == "Oscar Winner":
badge_class = "badge-oscar"
badge_text = "Oscar Winner"
elif movie["award_category"] == "Award Winner":
badge_class = "badge-award"
badge_text = "Award Winner"
elif movie["award_category"] == "Nominated":
badge_class = "badge-nominated"
badge_text = "Nominated"
badge_html = f'<span class="badge {badge_class}">{badge_text}</span>' if badge_text else ""
movie_cards += f"""
<div class="movie-card" data-rating="{movie['rating']}" data-year="{movie['year']}" data-category="{movie['award_category']}">
<img src="{movie['poster_url']}" alt="{movie['title']} poster" class="poster">
<div class="movie-info">
<h2>{movie['title']} <span class="year">({movie['year']})</span></h2>
<div class="rating">TMDB Rating: {movie['rating']}/10</div>
<div class="detail"><strong>Director:</strong> {movie['director']}</div>
<div class="detail"><strong>Studio:</strong> {movie['studio']}</div>
<div class="detail"><strong>Awards:</strong> {movie['awards_raw']}</div>
{badge_html}
</div>
</div>
"""
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crime Drama Awards Tracker</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #1a1a2e;
color: #eee;
padding: 2rem;
}}
.header {{
text-align: center;
margin-bottom: 2rem;
padding: 2rem;
background: linear-gradient(135deg, #16213e, #0f3460);
border-radius: 12px;
}}
.header h1 {{
font-size: 2.2rem;
margin-bottom: 0.5rem;
color: #e94560;
}}
.stats {{
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 1rem;
}}
.stat {{
text-align: center;
}}
.stat-number {{
font-size: 2rem;
font-weight: bold;
color: #e94560;
}}
.stat-label {{
font-size: 0.9rem;
color: #aaa;
}}
.movie-grid {{
display: grid;
gap: 1.5rem;
}}
.movie-card {{
display: flex;
background: #16213e;
border-radius: 12px;
overflow: hidden;
transition: transform 0.2s;
}}
.movie-card:hover {{
transform: translateY(-4px);
}}
.poster {{
width: 150px;
min-height: 225px;
object-fit: cover;
flex-shrink: 0;
}}
.movie-info {{
padding: 1.2rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}}
.movie-info h2 {{
font-size: 1.3rem;
color: #fff;
}}
.year {{
color: #aaa;
font-weight: normal;
font-size: 1rem;
}}
.rating {{
color: #e94560;
font-weight: bold;
font-size: 1.1rem;
}}
.detail {{
color: #ccc;
font-size: 0.95rem;
}}
.badge {{
display: inline-block;
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
margin-top: 0.5rem;
width: fit-content;
}}
.badge-oscar {{
background: linear-gradient(135deg, #f5af19, #f12711);
color: #fff;
}}
.badge-award {{
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
}}
.badge-nominated {{
background: linear-gradient(135deg, #4facfe, #00f2fe);
color: #1a1a2e;
}}
</style>
</head>
<body>
<div class="header">
<h1>Crime Drama Awards Tracker</h1>
<p>Top-rated crime drama films from the past 5 years</p>
<div class="stats">
<div class="stat">
<div class="stat-number">{len(movies)}</div>
<div class="stat-label">Films Found</div>
</div>
<div class="stat">
<div class="stat-number">{award_winners}</div>
<div class="stat-label">Award Winners</div>
</div>
<div class="stat">
<div class="stat-number">{oscar_winners}</div>
<div class="stat-label">Oscar Winners</div>
</div>
</div>
</div>
<div class="movie-grid">
{movie_cards}
</div>
</body>
</html>"""
filename = "crime_drama_report.html"
with open(filename, "w", encoding="utf-8") as f:
f.write(html)
print(f"\nReport saved to {filename}")
return filename
# --- Main ---
def main():
print("=" * 50)
print(" Crime Drama Awards Tracker")
print("=" * 50)
print("\n[1/3] Discovering top crime drama films...")
movies = discover_crime_dramas()
print("\n[2/3] Enriching with studio, director & award data...")
enriched_movies = enrich_movies(movies)
print("\n[3/3] Generating HTML report...")
filename = generate_html_report(enriched_movies)
filepath = os.path.abspath(filename)
webbrowser.open(f"file://{filepath}")
print(f"Report opened in your browser!")
if __name__ == "__main__":
main()
Your Crime Drama Awards Tracker is complete. You have a script that queries live movie data from two APIs and produces a polished visual report. Next up, try the Secret Mission to add interactive sorting and filtering controls to your report.
Secret mission
Add Interactive Sorting and Filtering
Your report looks great, but right now it's a static page. What if a viewer wants to see only Oscar-winning films, or sort by newest release? In this secret mission, you'll embed JavaScript directly into your generated HTML to add real-time filtering and sorting controls.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing cloud costs. Decide whether to keep your project folder for reference or delete it entirely.
Resources you used:
- The crime-drama-tracker project folder (contains your Python script, generated HTML report, and virtual environment).
What about your API keys?
Your TMDB and OMDb API keys remain active after this project. The TMDB key is free with no expiration. The OMDb free key allows 1,000 requests per day at no cost.
You can leave both keys active for future projects without worrying about charges.
Keep everything running
No action needed. Choose this if you want to keep your project files for reference or plan to extend the script further.
- Your crime-drama-tracker folder stays on your Desktop with all files intact.
- The virtual environment inside the folder takes up some disk space but costs nothing.
- Your TMDB and OMDb API keys remain active and free to use.
Pause - I'll come back to this later
Deactivate your virtual environment to free up your terminal session. Your files stay intact for when you return.
- If your virtual environment is still active in the terminal, deactivate it by running:
deactivate
- Your project folder and all files remain on your Desktop for later use.
Delete - I don't want to use this again
Remove all project files from your machine. This deletes the Python script, the generated HTML report, and the virtual environment.
- If your virtual environment is still active, deactivate it first by running:
deactivate
- Navigate to your Desktop and delete the project folder.
macOS
cd ~/Desktop
rm -rf crime-drama-tracker
Windows
cd ~\Desktop
rmdir /s /q crime-drama-tracker
- Confirm the folder is gone by running ls (macOS) or dir (Windows) on your Desktop. You should no longer see crime-drama-tracker in the output.
That's a Wrap!
That's a Wrap!
Nice work! You've just built a Python script that pulls live data from two movie APIs and produces a polished HTML report of the best crime dramas from the past five years.
You've learned how to:
- Queried the TMDB Discover API to find top-rated crime drama films using genre filtering, vote thresholds, and date ranges, then retrieved production company data from the movie details endpoint.
- Enriched each film with director and award information from the OMDb API, and parsed unstructured award strings to categorize films as Oscar winners, award winners, or nominated.
- Generated a styled HTML report with movie poster images, ratings, directors, studios, and colour-coded award badges.
- Secret Mission: Added interactive JavaScript-powered sorting and filtering controls to the HTML report.
Ready to quiz yourself?