439 lines
16 KiB
Python
Executable File
439 lines
16 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Create a new post for National Film Registry movies in the "Found in the Darkroom" series.
|
|
|
|
Usage:
|
|
python scripts/new_nfr.py tt1234567 # From IMDB ID
|
|
python scripts/new_nfr.py "Movie Title" # From title (searches TMDB)
|
|
python scripts/new_nfr.py --list-2024 # Show 2024 NFR list
|
|
python scripts/new_nfr.py --nfr-year 2024 # Set NFR induction year
|
|
|
|
The script will:
|
|
1. Fetch movie data from TMDB (poster, year, director, runtime, genres)
|
|
2. Download the poster
|
|
3. Create a draft post using the darkroom archetype
|
|
4. Pre-fill metadata including NFR year
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
|
|
# Configuration
|
|
try:
|
|
from config import TMDB_API_KEY
|
|
except ImportError:
|
|
raise SystemExit("Error: scripts/config.py not found. Copy config.example.py to config.py and add your API key.")
|
|
|
|
# Paths (relative to script location)
|
|
SCRIPT_DIR = Path(__file__).parent
|
|
PROJECT_ROOT = SCRIPT_DIR.parent
|
|
CONTENT_DIR = PROJECT_ROOT / "content" / "posts"
|
|
IMAGES_DIR = PROJECT_ROOT / "static" / "images" / "posters"
|
|
|
|
# 2024 National Film Registry inductees with LOC descriptions
|
|
# Source: https://newsroom.loc.gov/news/25-films-named-to-national-film-registry-for-preservation/
|
|
NFR_2024 = {
|
|
"Annabelle Serpentine Dance": {
|
|
"year": 1895,
|
|
"description": 'Preserved as a foundational cinema work that "enticed and enchanted audiences" during film\'s infancy, demonstrating early technical innovations like hand-tinted color.'
|
|
},
|
|
"Koko's Earth Control": {
|
|
"year": 1928,
|
|
"description": "Selected for representing the Fleischer Studios' competitive animation style against Disney, featuring innovative techniques like rotoscoping that advanced the medium."
|
|
},
|
|
"Angels with Dirty Faces": {
|
|
"year": 1938,
|
|
"description": 'Recognized for depicting "Depression-era immigrant, segregated, hardscrabble neighborhoods" while navigating Production Code restrictions through redemptive storytelling.'
|
|
},
|
|
"The Pride of the Yankees": {
|
|
"year": 1942,
|
|
"description": "Honored as one of cinema's seminal sports films, featuring authentic appearances by former Yankees teammates and Lou Gehrig's iconic farewell speech recreation."
|
|
},
|
|
"Invaders from Mars": {
|
|
"year": 1953,
|
|
"description": 'Selected for establishing "the visual language of science fiction cinema" and influencing subsequent sci-fi works through post-war paranoia themes.'
|
|
},
|
|
"The Miracle Worker": {
|
|
"year": 1962,
|
|
"description": 'Preserved for Arthur Penn\'s "stark black and white" presentation of Helen Keller\'s story, told with minimal sentimentality to highlight human potential.'
|
|
},
|
|
"The Chelsea Girls": {
|
|
"year": 1966,
|
|
"description": 'Recognized as a Warhol experimental work that challenged narrative form through dual-projection and "infinite audience interpretations."'
|
|
},
|
|
"Ganja and Hess": {
|
|
"year": 1973,
|
|
"description": 'Honored for addressing "complexities of addiction, sexuality and Black identity" through Bill Gunn\'s visionary filmmaking that remained underrecognized.'
|
|
},
|
|
"The Texas Chain Saw Massacre": {
|
|
"year": 1974,
|
|
"description": 'Selected for establishing "tenets of the gore/slasher/splatter genre" despite initial controversy, becoming a "cultural and filmmaking touchstone."'
|
|
},
|
|
"Uptown Saturday Night": {
|
|
"year": 1974,
|
|
"description": 'Preserved as Sidney Poitier\'s directorial effort "dispelling stereotypes" of the Blaxploitation era through an entertaining crime comedy ensemble cast.'
|
|
},
|
|
"Zora Lathan Student Films": {
|
|
"year": 1975,
|
|
"description": "Six short films recognized for showcasing filmmaking techniques and design problem-solving approaches, documenting intimate domestic moments from early 1980s perspectives."
|
|
},
|
|
"Up in Smoke": {
|
|
"year": 1978,
|
|
"description": 'Selected for arguably establishing the "stoner" film genre and paving "the way for subsequent memorable movie characters" through comic improvisation.'
|
|
},
|
|
"Will": {
|
|
"year": 1981,
|
|
"description": 'Honored as "the first independent feature-length film directed by a Black woman," documenting early 1980s Harlem while addressing addiction and resilience themes.'
|
|
},
|
|
"Star Trek: The Wrath of Khan": {
|
|
"year": 1982,
|
|
"description": 'Preserved as "often considered the best of the six original-cast Star Trek theatrical films," featuring expert direction and exploration of sacrifice.'
|
|
},
|
|
"Beverly Hills Cop": {
|
|
"year": 1984,
|
|
"description": 'Recognized as "Eddie Murphy\'s first feature film on the registry" and establishing his "box-office superstar" status through this buddy-cop action-comedy.'
|
|
},
|
|
"Dirty Dancing": {
|
|
"year": 1987,
|
|
"description": 'Selected for remaining "influential and imitated" despite addressing serious themes including pregnancy, abortion, and breaking class barriers through dance.'
|
|
},
|
|
"Common Threads: Stories from the Quilt": {
|
|
"year": 1989,
|
|
"description": 'Honored as an Oscar-winning documentary serving as "a monument to the power of grief and activism" chronicling the AIDS Memorial Quilt\'s creation.'
|
|
},
|
|
"Powwow Highway": {
|
|
"year": 1989,
|
|
"description": 'Preserved as "one of the first" films treating "Native Americans as ordinary people," departing from Hollywood stereotypes through a witty buddy road narrative.'
|
|
},
|
|
"My Own Private Idaho": {
|
|
"year": 1991,
|
|
"description": 'Recognized for Gus Van Sant\'s "magnificently original cult classic" reimagining Shakespeare through street hustlers\' journeys with "dream-like vision and hardcore reality."'
|
|
},
|
|
"American Me": {
|
|
"year": 1992,
|
|
"description": 'Selected for Edward James Olmos\'s directorial debut depicting "dark, brutal realities of Chicano gang life" addressing prison drug trafficking with unflinching honesty.'
|
|
},
|
|
"Mi Familia": {
|
|
"year": 1995,
|
|
"description": 'Preserved as Gregory Nava\'s "emotional and evocative" multi-generational Mexican-American family story celebrating immigration\'s role in American vitality.'
|
|
},
|
|
"Compensation": {
|
|
"year": 1999,
|
|
"description": 'Honored for director Zeinabu irene Davis\'s innovative accessibility approach incorporating "American Sign Language and title cards" for deaf and hearing audiences.'
|
|
},
|
|
"Spy Kids": {
|
|
"year": 2001,
|
|
"description": 'Selected for Robert Rodriguez\'s incorporation of "Hispanic culture" through family-centered storytelling emphasizing "familial bonds and cultural heritage" authenticity.'
|
|
},
|
|
"No Country for Old Men": {
|
|
"year": 2007,
|
|
"description": 'Preserved as a Coen Brothers modern-day Western adaptation "hailed as a classic nearly from the moment of release," winning Best Picture Oscar recognition.'
|
|
},
|
|
"The Social Network": {
|
|
"year": 2010,
|
|
"description": 'Recognized for transforming a potentially "dry, geeky" corporate narrative into "a riveting examination" of modern business ethics and technology\'s societal impact.'
|
|
},
|
|
}
|
|
|
|
|
|
def slugify(title):
|
|
"""Convert title to URL-friendly slug."""
|
|
slug = title.lower()
|
|
slug = re.sub(r"[^a-z0-9\s-]", "", slug)
|
|
slug = re.sub(r"[\s_]+", "-", slug)
|
|
slug = re.sub(r"-+", "-", slug)
|
|
return slug.strip("-")
|
|
|
|
|
|
def search_tmdb_by_title(title, year=None):
|
|
"""Search TMDB for a movie by title and optionally year."""
|
|
url = "https://api.themoviedb.org/3/search/movie"
|
|
params = {
|
|
"api_key": TMDB_API_KEY,
|
|
"query": title,
|
|
}
|
|
if year:
|
|
params["year"] = year
|
|
|
|
resp = requests.get(url, params=params, timeout=10)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
if not data.get("results"):
|
|
return None
|
|
|
|
# Return the first result
|
|
return data["results"][0]
|
|
|
|
|
|
def get_tmdb_details(tmdb_id):
|
|
"""Fetch movie details from TMDB."""
|
|
url = f"https://api.themoviedb.org/3/movie/{tmdb_id}"
|
|
params = {"api_key": TMDB_API_KEY}
|
|
resp = requests.get(url, params=params, timeout=10)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
def get_imdb_id_from_tmdb(tmdb_id):
|
|
"""Get IMDB ID from TMDB ID."""
|
|
data = get_tmdb_details(tmdb_id)
|
|
return data.get("imdb_id", "")
|
|
|
|
|
|
def get_tmdb_id_from_imdb(imdb_id):
|
|
"""Convert IMDB ID to TMDB ID."""
|
|
url = f"https://api.themoviedb.org/3/find/{imdb_id}"
|
|
params = {
|
|
"api_key": TMDB_API_KEY,
|
|
"external_source": "imdb_id",
|
|
}
|
|
resp = requests.get(url, params=params, timeout=10)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
results = data.get("movie_results", [])
|
|
if not results:
|
|
raise ValueError(f"No TMDB match found for IMDB ID: {imdb_id}")
|
|
|
|
return results[0]["id"]
|
|
|
|
|
|
def download_poster(poster_path, filename):
|
|
"""Download poster from TMDB to static/images/posters/."""
|
|
if not poster_path:
|
|
print(" No poster available")
|
|
return None
|
|
|
|
# Use w500 size for good quality without being huge
|
|
url = f"https://image.tmdb.org/t/p/w500{poster_path}"
|
|
resp = requests.get(url, timeout=10)
|
|
resp.raise_for_status()
|
|
|
|
IMAGES_DIR.mkdir(parents=True, exist_ok=True)
|
|
filepath = IMAGES_DIR / filename
|
|
filepath.write_bytes(resp.content)
|
|
print(f" Poster saved: {filepath.relative_to(PROJECT_ROOT)}")
|
|
return f"/images/posters/{filename}"
|
|
|
|
|
|
def extract_imdb_id(input_str):
|
|
"""Extract IMDB ID from string (handles raw ID or URL)."""
|
|
# Check if it's already just an ID
|
|
if re.match(r'^tt\d+$', input_str):
|
|
return input_str
|
|
|
|
# Try to extract from URL
|
|
match = re.search(r'(tt\d+)', input_str)
|
|
if match:
|
|
return match.group(1)
|
|
|
|
return None
|
|
|
|
|
|
def format_director(directors):
|
|
"""Format director(s) for YAML frontmatter."""
|
|
if not directors:
|
|
return '""'
|
|
if len(directors) == 1:
|
|
return f'"{directors[0]}"'
|
|
# Multiple directors - use YAML list format
|
|
return "[" + ", ".join(f'"{d}"' for d in directors) + "]"
|
|
|
|
|
|
def create_nfr_post(tmdb_data, imdb_id, nfr_year=2024):
|
|
"""Create a draft post for an NFR movie."""
|
|
title = tmdb_data.get("title", "Unknown")
|
|
slug = slugify(title)
|
|
filename = f"{slug}.md"
|
|
filepath = CONTENT_DIR / filename
|
|
|
|
if filepath.exists():
|
|
print(f" Post already exists: {filepath.relative_to(PROJECT_ROOT)}")
|
|
overwrite = input(" Overwrite? (y/N): ").strip().lower()
|
|
if overwrite != 'y':
|
|
return None
|
|
|
|
# Format the date for Hugo
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
# Extract metadata
|
|
year = tmdb_data.get("release_date", "")[:4] if tmdb_data.get("release_date") else ""
|
|
runtime = tmdb_data.get("runtime", "")
|
|
overview = tmdb_data.get("overview", "")
|
|
|
|
# Get directors from crew
|
|
directors = []
|
|
# Note: Full crew info requires a second API call, so we'll leave it blank for now
|
|
# Users can fill it in or we can enhance this later
|
|
|
|
# Genres
|
|
genres = [g["name"] for g in tmdb_data.get("genres", [])]
|
|
genres_yaml = "[" + ", ".join(genres) + "]" if genres else "[]"
|
|
|
|
# Poster
|
|
poster_url = ""
|
|
if tmdb_data.get("poster_path"):
|
|
print(" Downloading poster...")
|
|
poster_filename = f"{slug}.jpg"
|
|
poster_url = download_poster(tmdb_data["poster_path"], poster_filename)
|
|
|
|
# Look up LOC description if this is a 2024 NFR film
|
|
loc_description = ""
|
|
if nfr_year == 2024:
|
|
# Try to match the title to our NFR_2024 dictionary
|
|
for nfr_title, nfr_data in NFR_2024.items():
|
|
if title.lower() in nfr_title.lower() or nfr_title.lower() in title.lower():
|
|
loc_description = nfr_data["description"]
|
|
print(f" Found LOC description for NFR 2024: {nfr_title}")
|
|
break
|
|
|
|
# Build NFR section content
|
|
if loc_description:
|
|
nfr_section = f"""## Why It's in the National Film Registry
|
|
|
|
{loc_description}
|
|
|
|
*Source: [Library of Congress National Film Registry 2024 announcement](https://newsroom.loc.gov/news/25-films-named-to-national-film-registry-for-preservation/)*"""
|
|
else:
|
|
nfr_section = """## Why It's in the National Film Registry
|
|
|
|
[Add information about why this film was selected for preservation by the Library of Congress]"""
|
|
|
|
# Build the frontmatter and content
|
|
content = f'''---
|
|
title: '{title}'
|
|
date: {now}
|
|
draft: true
|
|
series: "Found in the Darkroom"
|
|
summary: ""
|
|
imdb: "{imdb_id}"
|
|
poster: "{poster_url or ''}"
|
|
year: {year}
|
|
runtime: {runtime}
|
|
director: ""
|
|
genres: {genres_yaml}
|
|
nfr_year: {nfr_year}
|
|
letterboxd_url: ""
|
|
tags:
|
|
- national-film-registry
|
|
- home-video
|
|
---
|
|
{{{{< imdbposter >}}}}
|
|
|
|
| Date watched | |
|
|
|------------------------|-----------------------|
|
|
| Format | |
|
|
| Watched Multiple Times | |
|
|
| Added to NFR | {nfr_year} |
|
|
| Letterboxd Rating | |
|
|
| Personal Notes | |
|
|
|
|
{{{{< /imdbposter >}}}}
|
|
|
|
{nfr_section}
|
|
|
|
## My Thoughts
|
|
|
|
{overview}
|
|
|
|
'''
|
|
|
|
filepath.write_text(content)
|
|
print(f" Draft created: {filepath.relative_to(PROJECT_ROOT)}")
|
|
print(f"\nNext steps:")
|
|
print(f" 1. Fill in director and other metadata by running:")
|
|
print(f" python scripts/fetch_movie_data.py")
|
|
print(f" 2. Add your viewing details and thoughts")
|
|
if not loc_description:
|
|
print(f" 3. Research why it was added to the NFR")
|
|
print(f" {'4' if not loc_description else '3'}. Add your Letterboxd URL if you've logged it there")
|
|
|
|
return filepath
|
|
|
|
|
|
def list_nfr_2024():
|
|
"""Display the 2024 NFR inductees."""
|
|
print("\n2024 National Film Registry Inductees:\n")
|
|
for i, (title, data) in enumerate(NFR_2024.items(), 1):
|
|
print(f" {i:2}. {title} ({data['year']})")
|
|
print()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Create NFR movie posts for 'Found in the Darkroom' series"
|
|
)
|
|
parser.add_argument("input", nargs="?", help="IMDB ID (tt1234567) or movie title")
|
|
parser.add_argument("--list-2024", action="store_true", help="List 2024 NFR inductees")
|
|
parser.add_argument("--nfr-year", type=int, default=2024, help="NFR induction year (default: 2024)")
|
|
args = parser.parse_args()
|
|
|
|
if args.list_2024:
|
|
list_nfr_2024()
|
|
sys.exit(0)
|
|
|
|
if not args.input:
|
|
print("Error: Please provide an IMDB ID or movie title")
|
|
print("\nUsage:")
|
|
print(" python scripts/new_nfr.py tt1234567")
|
|
print(" python scripts/new_nfr.py 'No Country for Old Men'")
|
|
print(" python scripts/new_nfr.py --list-2024")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
# Try to extract IMDB ID
|
|
imdb_id = extract_imdb_id(args.input)
|
|
|
|
if imdb_id:
|
|
print(f"Looking up movie by IMDB ID: {imdb_id}")
|
|
tmdb_id = get_tmdb_id_from_imdb(imdb_id)
|
|
tmdb_data = get_tmdb_details(tmdb_id)
|
|
else:
|
|
# Assume it's a title search
|
|
print(f"Searching for: {args.input}")
|
|
# Try to find year in NFR list
|
|
year_hint = None
|
|
for title, data in NFR_2024.items():
|
|
if args.input.lower() in title.lower() or title.lower() in args.input.lower():
|
|
year_hint = data["year"]
|
|
print(f"Found in NFR 2024 list: {title} ({data['year']})")
|
|
break
|
|
|
|
search_result = search_tmdb_by_title(args.input, year_hint)
|
|
if not search_result:
|
|
print(f"Error: No movie found for '{args.input}'")
|
|
sys.exit(1)
|
|
|
|
print(f"Found: {search_result['title']} ({search_result.get('release_date', '')[:4]})")
|
|
confirm = input("Is this correct? (Y/n): ").strip().lower()
|
|
if confirm == 'n':
|
|
print("Search cancelled")
|
|
sys.exit(0)
|
|
|
|
tmdb_id = search_result["id"]
|
|
tmdb_data = get_tmdb_details(tmdb_id)
|
|
imdb_id = tmdb_data.get("imdb_id", "")
|
|
|
|
if not imdb_id:
|
|
print("Warning: No IMDB ID found for this movie")
|
|
|
|
print(f"\nCreating post for: {tmdb_data.get('title')}")
|
|
create_nfr_post(tmdb_data, imdb_id, args.nfr_year)
|
|
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|