Updated the metadata fetching script and wrote the first National Film Registry entry

This commit is contained in:
mnw
2026-01-01 22:25:07 -06:00
parent 82e01341c9
commit 3bcecdd7b5
25 changed files with 1753 additions and 87 deletions
+41
View File
@@ -0,0 +1,41 @@
---
title: '{{ replace .Name "-" " " | title }}'
date: {{ .Date }}
draft: true
series: "Found in the Darkroom"
summary: "TODO: Add a summary for the homepage"
# Fill in IMDB ID, then run: python scripts/fetch_movie_data.py
imdb: ""
# Auto-filled by fetch_movie_data.py:
poster: ""
year:
runtime:
director: ""
genres: []
# National Film Registry info
nfr_year: 2024
letterboxd_url: ""
tags:
- national-film-registry
- home-video
---
{{< imdbposter >}}
| Date watched | |
|------------------------|-----------------------|
| Format | |
| Watched Multiple Times | |
| Added to NFR | {{ .Params.nfr_year }} |
| Letterboxd Rating | |
| Personal Notes | |
{{< /imdbposter >}}
## Why It's in the National Film Registry
[Add information about why this film was selected for preservation]
## My Thoughts
This is where our review goes and we talk about the film, its historical significance, and how it holds up today.
+15 -7
View File
@@ -1,13 +1,21 @@
---
title: 'Avatar Fire and Ash'
date: 2025-12-23T03:44:12Z
title: Avatar Fire and Ash
date: 2025-12-23 03:44:12+00:00
draft: false
series: "Frank's Couch"
summary: "James Cameron and the boys enjoy a hot winter evening."
imdb: "tt1757678"
series: Frank's Couch
summary: James Cameron and the boys enjoy a hot winter evening.
imdb: tt1757678
tags:
- ghost theater
- anticipated
- ghost theater
- anticipated
poster: /images/posters/avatar-fire-and-ash.jpg
runtime: 198
year: 2025
director: James Cameron
genres:
- Science Fiction
- Adventure
- Fantasy
---
{{< imdbposter >}}
+19 -8
View File
@@ -1,14 +1,25 @@
---
title: 'Cloud Atlas Pancake'
date: 2024-11-20T14:49:23Z
title: Cloud Atlas Pancake
date: 2024-11-20 14:49:23+00:00
draft: true
series: "Frank's Couch"
summary: "First post about a Master Pancake movie. Cloud Atlas was confusing but it was fun!"
imdb: "tt1371111"
series: Frank's Couch
summary: First post about a Master Pancake movie. Cloud Atlas was confusing but it
was fun!
imdb: tt1371111
tags:
- CYOP
- mueller
- no-pizza
- CYOP
- mueller
- no-pizza
poster: /images/posters/cloud-atlas-pancake.jpg
runtime: 172
year: 2012
director:
- Lilly Wachowski
- Lana Wachowski
- Tom Tykwer
genres:
- Drama
- Science Fiction
---
{{< imdbposter >}}
+16 -8
View File
@@ -1,14 +1,22 @@
---
title: 'Joker Folie a Deux'
date: 2024-10-07T00:03:15Z
title: Joker Folie a Deux
date: 2024-10-07 00:03:15+00:00
draft: false
series: "Frank's Couch"
summary: "The boys get to B serious about Joker 2"
imdb: "tt11315808"
series: Frank's Couch
summary: The boys get to B serious about Joker 2
imdb: tt11315808
tags:
- gucci
- anticipated
- no pizza
- gucci
- anticipated
- no pizza
poster: /images/posters/joker-folie-a-deux.jpg
runtime: 138
year: 2024
director: Todd Phillips
genres:
- Drama
- Crime
- Thriller
---
{{< imdbposter >}}
+13 -6
View File
@@ -1,16 +1,23 @@
---
title: 'Megalopolis'
date: 2024-10-02T00:07:14Z
title: Megalopolis
date: 2024-10-02 00:07:14+00:00
draft: false
series: "Frank's Couch"
summary: "The boys catch Megalopolis at Gucci on a Saturday Afternoon."
imdb: "tt10128846"
series: Frank's Couch
summary: The boys catch Megalopolis at Gucci on a Saturday Afternoon.
imdb: tt10128846
tags:
- gucci
- anticipated
- had pizza
poster: /images/posters/megalopolis.jpg
runtime: 138
year: 2024
director: Francis Ford Coppola
genres:
- Science Fiction
- Drama
- Fantasy
---
{{< imdbposter >}}
| Date watched | September 28 |
+15 -9
View File
@@ -1,15 +1,21 @@
---
title: 'Terrifier 3'
date: 2024-10-15T00:28:11Z
title: Terrifier 3
date: 2024-10-15 00:28:11+00:00
draft: false
series: "Frank's Couch"
summary: "The Boys get terrified, late at night."
imdb: "tt27911000"
series: Frank's Couch
summary: The Boys get terrified, late at night.
imdb: tt27911000
tags:
- ghost theater
- anticipated
- no-pizza
- ghost theater
- anticipated
- no-pizza
poster: /images/posters/terrifier-3.jpg
runtime: 125
year: 2024
director: Damien Leone
genres:
- Horror
- Thriller
---
{{< imdbposter >}}
+15 -14
View File
@@ -1,20 +1,21 @@
---
title: 'The Housemaid'
date: 2026-01-01T05:54:14Z
title: The Housemaid
date: 2026-01-01 05:54:14+00:00
draft: false
series: "Frank's Couch"
summary: "Marcus goes it alone on New Year's Eve to learn about manipulation and the cost of privilege."
imdb: "tt27543632"
poster: "/images/posters/the-housemaid.jpg"
series: Frank's Couch
summary: Marcus goes it alone on New Year's Eve to learn about manipulation and the
cost of privilege.
imdb: tt27543632
poster: /images/posters/the-housemaid.jpg
tags:
- marcel
- no-expectations
# Mastodon comments: After posting about this on Mastodon, add the post ID below.
# Get the ID from the end of the toot URL, e.g. https://tilde.zone/@mnw/123456789
# mastodon_id: ""
# To block a reply from showing, add its full URL to this list:
# mastodon_blocked:
# - "https://tilde.zone/@someone/123456789"
- marcel
- no-expectations
runtime: 131
year: 2025
director: Paul Feig
genres:
- Mystery
- Thriller
---
{{< imdbposter >}}
+16 -14
View File
@@ -1,20 +1,22 @@
---
title: 'The Secret Agent'
date: 2025-12-26T00:37:12Z
title: The Secret Agent
date: 2025-12-26 00:37:12+00:00
draft: false
series: "Frank's Couch"
summary: "Its Christmas lets go watch a movie or two. Secret Agent is a Brazilian 70s Spy Thriller"
imdb: "tt27847051"
poster: "/images/posters/the-secret-agent.jpg"
series: Frank's Couch
summary: Its Christmas lets go watch a movie or two. Secret Agent is a Brazilian 70s
Spy Thriller
imdb: tt27847051
poster: /images/posters/the-secret-agent.jpg
tags:
- no-expectations
- alamo-drafthouse
# Mastodon comments: After posting about this on Mastodon, add the post ID below.
# Get the ID from the end of the toot URL, e.g. https://tilde.zone/@mnw/123456789
# mastodon_id: ""
# To block a reply from showing, add its full URL to this list:
# mastodon_blocked:
# - "https://tilde.zone/@someone/123456789"
- no-expectations
- alamo-drafthouse
runtime: 161
year: 2025
director: Kleber Mendonça Filho
genres:
- Crime
- Drama
- Thriller
---
{{< imdbposter >}}
+88
View File
@@ -0,0 +1,88 @@
---
title: Uptown Saturday Night
date: 2026-01-02 04:00:57+00:00
draft: true
series: Found in the Darkroom
summary: 'We embark on our journey to watch movies of the National Film Archive starting with a fun to watch comedy from 1974.'
imdb: tt0072351
poster: /images/posters/uptown-saturday-night.jpg
year: 1974
runtime: 104
director: Sidney Poitier
genres:
- Comedy
- Crime
- Action
nfr_year: 2024
letterboxd_url: 'https://letterboxd.com/marcuseid/film/uptown-saturday-night/'
tags:
- national-film-registry
- home-video
---
{{< imdbposter >}}
| Date watched | |
|------------------------|-----------------------|
| Format | 4k TV w/Panasonic BRPlayer |
| Watched Multiple Times | Second Watch |
| Added to NFR | 2024 |
| Letterboxd Rating | **** (4.0) |
| Personal Notes | Fun and full of laughs |
{{< /imdbposter >}}
## Why It's in the National Film Registry
Preserved as Sidney Poitier's directorial effort "dispelling stereotypes" of the Blaxploitation era through an entertaining crime comedy ensemble cast.
*Source: [Library of Congress National Film Registry 2024 announcement](https://newsroom.loc.gov/news/25-films-named-to-national-film-registry-for-preservation/)*
## My Thoughts
My friend Mehone recommended Uptown Saturday Night to me a year or two ago. I watched it back then and enjoyed it, but I never logged it or wrote about it; it was just a fun weekend movie at the time. However, now that Im working my way through the National Film Registry (starting with the 2024 additions), I decided to revisit it, and Im finding a lot to like.
**The Setup**
The plot follows Steve Jackson (Sidney Poitier), a blue-collar worker who has two weeks of vacation. It looks like a foundry of some kind but I believe they call it a factory in the movie for some reason it's something that is sticking in my mind. Since his wife is still working, he plans to just relax around the house. However, his best friend, Wardell Franklin (a young _beareded_ Bill Cosby), thinks Steve needs to unwind a little more aggressively.
Wardell suggests going to Madame Zenobias, a high-class, after-hours club. Everyone knows about it, but not everyone can get in. Wardell manages to forge a letter of introduction using his wife's employer's stationery, claiming they are important players in the diamond business. Surprisingly, this gets them in the door.
The Incident
The club is a showcase of the hottest looks of the era—big hats, wonderful hairstyles, and wild outfits. Steve and Wardell eventually find their way to a room behind the red door where gambling is taking place. Its a high-stakes environment; the bouncer warns them that loitering isnt allowed and the buy-in is high. Steve lends Wardell some cash, they start betting, and suddenly, they are on a hot streak.
Unfortunately, the celebration is cut short when a crew busts in to rob the place. In a strange twist, or maybe it was just a thing they did at the time, the robbers force everyone to strip down to their underwear. They say in case one of you has a heater which I think means a concealed weapon. It also helps to prevent anyone from chasing them immediately. (One woman claims she isn't wearing underwear, but they make her strip anyway—though the movie keeps it PG and doesn't show anything explicit).
The robbery sets up the central conflict: Steve had a lottery ticket in his wallet containing his lucky numbers. The next day, he sees those numbers hit the jackpot in the newspaper, and realizes the ticket is lost! The ticket is worth $50,000 (which is over $320,000 adjusted for inflation). Its life changing amount money that would allow his family to move out of the inner city. The rest of the movie is a hilarious race to find the wallet without letting anyone know what is in it.
**A Star-Studded Cast**
The film features a massive roster of wonderful actors, and everyone gets a moment to shine.
Harry Belafonte plays Geechee Dan, a menacing gangster who looks like he has bad allergies or is just really sick.
Flip Wilson plays the Reverend who preaches "No joy juice at the picknic!"
Richard Pryor has a cameo as Sharp Eye Washington, a con man, who posed as a private eye briefly.
Calvin Lockhart plays Silky Slim a rival gangster to Geechee Dan.
Rosalind Cash plays Steves wife, Sarah Jackson. She has some of the best lines!
Paula Kelly and Lee Chamberlin (Madame Zenobia) also turn in great performances.
One of the funniest performances comes from Roscoe Lee Browne, who plays Congressman Lincoln. His character satirizes politicians embracing the Black Power movement for votes. When constituents arrive, he hurriedly flips a picture of Richard Nixon around to reveal a portrait of Malcolm X and changes from a suit into a dashiki to play the part. It was a hilarious reminder of how post-Watergate movies depicted politicians as chameleons willing to say anything for a vote.
The Elephant in the Room
It is impossible to discuss this film without acknowledging that Bill Cosby has since been convicted of heinous sex crimes. However, looking strictly at the film as a 1970s comedy, the character he plays does not have any romantic entanglements or questionable "adult" situations. If you can, as they say, separate the art from the artist's off-screen behavior, his dynamic with Poitier is genuinely funny.
**The 70s Aesthetic**
There are some very specific "70s things" in this movie that I really enjoyed. For one, the church picnic scene is massive. The church actually owns a Greyhound-style bus to transport the congregation to a fairground for food and games—it really captures the community vibe of the era.
The fashion is also incredible. As a child of the 80s and 90s, I used to think bellbottoms and wide lapels looked ridiculous. Watching it now, I totally get it. The lines, the colors, and the energy that the right pair of shoes or jacket conveyed allowed people to really stand out. Plus, the slang is a blast to listen to—you don't hear people getting called "turkeys" enough anymore.
Preservation and Quality
To wrap things up, I was struck by how well-preserved this film is. Because it is on film it was also possible to be scanned in at high resolution and detail. The audio is just as crisp as when it was recorded.
It makes me wonder about our current digital era. We record video just to compress it immediately. Many movies were left behind on VHS, then DVD, then Blu-ray. In 30 years, when we are watching on "2060's Hottest new Retina 20K" displays, our current digital footage might look blown out and pixelated. But Uptown Saturday Night? It will still look sharp.
+14 -14
View File
@@ -1,20 +1,20 @@
---
title: 'Urchin'
date: 2025-12-25T15:53:35Z
title: Urchin
date: 2025-12-25 15:53:35+00:00
draft: false
series: "Frank's Couch"
summary: "I watched movie that felt very real and led me to refelct on mylife. Felt almost like I could have escaped into the tv screen."
imdb: "tt35715953"
poster: "/images/posters/urchin.jpg"
series: Frank's Couch
summary: I watched movie that felt very real and led me to refelct on mylife. Felt
almost like I could have escaped into the tv screen.
imdb: tt35715953
poster: /images/posters/urchin.jpg
tags:
- homevideo
- no-expectations
# Mastodon comments: After posting about this on Mastodon, add the post ID below.
# Get the ID from the end of the toot URL, e.g. https://tilde.zone/@mnw/123456789
# mastodon_id: ""
# To block a reply from showing, add its full URL to this list:
# mastodon_blocked:
# - "https://tilde.zone/@someone/123456789"
- homevideo
- no-expectations
runtime: 100
year: 2025
director: Harris Dickinson
genres:
- Drama
---
{{< imdbposter >}}
+14 -7
View File
@@ -1,13 +1,20 @@
---
title: 'Will and Harper'
date: 2024-10-05T16:26:32Z
title: Will and Harper
date: 2024-10-05 16:26:32+00:00
draft: false
series: "Frank's Couch"
summary: "I watched Will and Harper at Home on a Sunday afternoon."
imdb: "tt30321133"
series: Frank's Couch
summary: I watched Will and Harper at Home on a Sunday afternoon.
imdb: tt30321133
tags:
- no-expectations
- no pizza
- no-expectations
- no pizza
poster: /images/posters/will-and-harper.jpg
runtime: 114
year: 2024
director: Josh Greenbaum
genres:
- Documentary
- Comedy
---
{{< imdbposter >}}
+37
View File
@@ -0,0 +1,37 @@
{{ define "main" -}}
<h1 class="title">{{ .Title }}</h1>
{{ if eq .Title "Found in the Darkroom" }}
<div style="margin-bottom: 2em; line-height: 1.6;">
<p><strong>Found in the Darkroom</strong> is a journey through the National Film Registry, where America's cinematic heritage is preserved one frame at a time.</p>
<p>The <a href="https://www.loc.gov/programs/national-film-preservation-board/film-registry/" target="_blank">National Film Registry</a> is maintained by the Library of Congress, which each year selects 25 films that are "culturally, historically, or aesthetically significant" for preservation. These aren't just the "best" films—they're the ones that matter, that capture moments in time, that changed how we see the world or simply how we see movies.</p>
<p>The "darkroom" is where film is developed, where latent images become visible, where memories are fixed and preserved. It's a fitting metaphor for revisiting these films: bringing them out of storage, letting them develop again in our minds, seeing what remains when we hold them up to the light today.</p>
<p>From the earliest experiments in motion pictures to modern classics, these are the films worth preserving, worth remembering, worth watching again.</p>
</div>
{{ end }}
<ul class="entries">
{{ range .Pages.GroupByDate "2006" }}
<h3 style="text-align: center;">{{ .Key }}</h3>
{{ range .Pages }}
<li>
<span class="title">
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
{{ if .Params.year }}
<span style="color: #888;">({{ .Params.year }})</span>
{{ end }}
{{ if .Params.nfr_year }}
<span style="color: #888; font-size: 0.9em;"> • NFR {{ .Params.nfr_year }}</span>
{{ end }}
</span>
<span class="published">
<time class="pull-right post-list">{{ .Date | time.Format ":date_long" }}</time>
</span>
</li>
{{ end }}
{{ end }}
</ul>
{{ end }}
+1
View File
@@ -1 +1,2 @@
requests
pyyaml
+341
View File
@@ -0,0 +1,341 @@
# National Film Registry Automation Guide
This guide explains how to automatically pull and setup data for National Film Registry movies from any year.
## Overview
The NFR automation system consists of:
1. **`setup_nfr.py`** - Script to fetch LOC announcements and extract film data
2. **`new_nfr.py`** - Script to create blog posts for NFR movies
3. **ollama** - Local AI to help extract structured data from web pages
## Quick Start
### Basic Usage
```bash
# Setup data for a specific year
python3 scripts/setup_nfr.py 2023
# With a known URL
python3 scripts/setup_nfr.py 2015 --url "https://newsroom.loc.gov/news/..."
# Without ollama (basic extraction)
python3 scripts/setup_nfr.py 2022 --no-ollama
```
### With Ollama (Recommended)
Ollama provides much better extraction of film descriptions from the LOC announcements.
```bash
# Default (uses ollama at 192.168.0.109:11434)
python3 scripts/setup_nfr.py 2023
# Custom ollama host
python3 scripts/setup_nfr.py 2023 --ollama-host http://localhost:11434
# Custom model
python3 scripts/setup_nfr.py 2023 --ollama-model llama3.2:latest
```
## Setting Up Ollama
### What is Ollama?
Ollama is a tool for running large language models locally. We use it to:
- Parse HTML content from LOC announcements
- Extract film titles, years, and descriptions
- Structure the data into Python dictionaries
### Installing Ollama
Your server at `192.168.0.109` should already have ollama running. To verify:
```bash
curl http://192.168.0.109:11434/api/tags
```
If you need to install it locally:
```bash
# macOS / Linux
curl https://ollama.ai/install.sh | sh
# Start the server
ollama serve
# Pull a model
ollama pull llama3.2
```
### Ollama Configuration
The script uses these environment variables:
```bash
# Set custom ollama host
export OLLAMA_HOST=http://192.168.0.109:11434
# Set custom model (default: llama3.2)
export OLLAMA_MODEL=llama3.2
# Then run the script
python3 scripts/setup_nfr.py 2023
```
### Testing Ollama Connection
Test if ollama is accessible:
```bash
# Test API endpoint
curl http://192.168.0.109:11434/api/tags
# Test generation
curl http://192.168.0.109:11434/api/generate -d '{
"model": "llama3.2",
"prompt": "Say hello",
"stream": false
}'
```
## How It Works
### Step 1: Find the LOC Announcement
The script needs the URL of the Library of Congress announcement for your year. For example:
- **2024**: https://newsroom.loc.gov/news/25-films-named-to-national-film-registry-for-preservation/s/55d5285d-916f-4105-b7d4-7fc3ba8664e3
- **2023**: Search at https://newsroom.loc.gov/
- **Older**: Check https://blogs.loc.gov/now-see-hear/
You can provide the URL with `--url` or the script will prompt you.
### Step 2: Fetch the Content
The script downloads the HTML content from the announcement page.
### Step 3: Extract Film Data
**With ollama (recommended):**
- Sends the HTML to ollama
- Asks it to extract all 25 films with titles, years, and descriptions
- Returns structured JSON data
**Without ollama (fallback):**
- Uses regex patterns to find film titles and years
- May miss descriptions or get incomplete data
- Requires manual review and editing
### Step 4: Generate Python Dictionary
Creates a Python file like:
```python
# 2023 National Film Registry inductees with LOC descriptions
# Source: https://newsroom.loc.gov/news/...
NFR_2023 = {
"Film Title": {
"year": 1999,
"description": "Selected for its groundbreaking..."
},
# ... more films
}
```
### Step 5: Integration
The generated file is saved to `scripts/nfr_data/nfr_YEAR.py`. You can then:
1. Review and edit the file
2. Copy the dictionary into `scripts/new_nfr.py`
3. Update the script to handle the new year
## Complete Example
Let's set up 2023 NFR data:
```bash
# 1. Run the setup script
python3 scripts/setup_nfr.py 2023
# The script will prompt:
# > Please find the LOC announcement URL for 2023.
# > Enter the URL: https://newsroom.loc.gov/news/...
# 2. Script fetches and extracts (using ollama)
# ✓ Extracted 25 films
# Preview:
# 1. Terminator 2 (1991)
# Recognized for groundbreaking visual effects...
# ... and 24 more
# 3. Confirm and save
# Save this data? (Y/n): y
# ✓ Saved to scripts/nfr_data/nfr_2023.py
# 4. Review the generated file
cat scripts/nfr_data/nfr_2023.py
# 5. Copy the dictionary into new_nfr.py
# (You can do this manually or we can create a script to merge)
```
## Directory Structure
```
scripts/
├── setup_nfr.py # Main automation script
├── new_nfr.py # Create blog posts
├── nfr_data/ # Generated NFR data files
│ ├── nfr_2023.py
│ ├── nfr_2024.py
│ └── ...
└── NFR_AUTOMATION.md # This file
```
## Troubleshooting
### Ollama Connection Errors
```bash
# Check if ollama is running
curl http://192.168.0.109:11434/api/tags
# Check network connectivity
ping 192.168.0.109
# Try with localhost if running locally
python3 scripts/setup_nfr.py 2023 --ollama-host http://localhost:11434
```
### Extraction Problems
If extraction fails:
```bash
# Try without ollama first (gets basic structure)
python3 scripts/setup_nfr.py 2023 --no-ollama
# Then manually edit the descriptions in nfr_data/nfr_2023.py
```
### Model Not Found
```bash
# On the ollama server, pull the model
ssh user@192.168.0.109
ollama pull llama3.2
# Or use a different model you have
python3 scripts/setup_nfr.py 2023 --ollama-model mistral
```
## Finding LOC Announcements
### Recent Years (2010-present)
Check the newsroom:
```
https://newsroom.loc.gov/
```
Search for "national film registry" + year
### Older Years
Check the blog:
```
https://blogs.loc.gov/now-see-hear/
```
Or the registry page:
```
https://www.loc.gov/programs/national-film-preservation-board/film-registry/
```
### Complete Registry List
For a complete list by year:
```
https://www.loc.gov/programs/national-film-preservation-board/film-registry/complete-national-film-registry-listing/
```
## Advanced Usage
### Custom Output Location
```bash
python3 scripts/setup_nfr.py 2023 \
--output /tmp/nfr_2023.py
```
### Batch Processing Multiple Years
```bash
# Create a simple loop
for year in 2020 2021 2022 2023; do
python3 scripts/setup_nfr.py $year
done
```
### Using Different AI Models
```bash
# Llama 3.2 (default, good balance)
python3 scripts/setup_nfr.py 2023 --ollama-model llama3.2
# Mistral (faster, less accurate)
python3 scripts/setup_nfr.py 2023 --ollama-model mistral
# Larger models for better extraction
python3 scripts/setup_nfr.py 2023 --ollama-model llama3.2:70b
```
## Integration with new_nfr.py
After generating NFR data, integrate it into `new_nfr.py`:
### Option 1: Manual Copy
1. Open `scripts/nfr_data/nfr_2023.py`
2. Copy the `NFR_2023` dictionary
3. Add it to `scripts/new_nfr.py` after `NFR_2024`
4. Update the `create_nfr_post` function to check `NFR_2023` too
### Option 2: Import (Future Enhancement)
```python
# In new_nfr.py
from nfr_data.nfr_2023 import NFR_2023
from nfr_data.nfr_2024 import NFR_2024
NFR_DATA = {
2023: NFR_2023,
2024: NFR_2024,
}
```
## Tips
1. **Always review the output** - AI extraction is good but not perfect
2. **Keep source URLs** - Add them to the generated dictionaries
3. **Check film counts** - Should be 25 films per year
4. **Verify years** - Make sure film years are in reasonable ranges
5. **Edit descriptions** - Feel free to trim or rephrase for your blog
## Next Steps
1. Generate data for years you want to cover
2. Review and edit the descriptions
3. Integrate into `new_nfr.py`
4. Start creating blog posts with `python3 scripts/new_nfr.py "Film Title"`
## Questions?
- Check if ollama is running: `curl http://192.168.0.109:11434/api/tags`
- Test the script with 2024 (known working): `python3 scripts/setup_nfr.py 2024`
- Use `--no-ollama` to see basic extraction
- Look at generated files in `scripts/nfr_data/`
+271
View File
@@ -0,0 +1,271 @@
# Blog Scripts
Automation scripts for The Double Lunch Dispatch blog.
## Quick Start
### Install Dependencies
```bash
# Create virtual environment (if not already done)
python3 -m venv .venv
# Activate it
source .venv/bin/activate
# Install dependencies
pip install -r requirements.txt
```
### Required: TMDB API Key
All movie scripts require a TMDB API key.
```bash
# Copy the example config
cp scripts/config.example.py scripts/config.py
# Edit and add your TMDB API key
# Get one at: https://www.themoviedb.org/settings/api
```
## Scripts Overview
### National Film Registry (NFR) Series
**"Found in the Darkroom"** - A series covering the National Film Registry
#### Setup NFR Data (New!)
Automatically fetch and setup data for any NFR year:
```bash
# Setup data for a specific year
python3 scripts/setup_nfr.py 2023
# With a known URL
python3 scripts/setup_nfr.py 2015 --url "https://newsroom.loc.gov/news/..."
# See full documentation
cat scripts/NFR_AUTOMATION.md
```
#### Create NFR Movie Post
Create a blog post for an NFR movie:
```bash
# List 2024 NFR films
python3 scripts/new_nfr.py --list-2024
# Create post by title
python3 scripts/new_nfr.py "No Country for Old Men"
# Create post by IMDB ID
python3 scripts/new_nfr.py tt0477348
# Specify NFR year
python3 scripts/new_nfr.py "Terminator 2" --nfr-year 2023
```
### Regular Movie Posts
**"Frank's Couch"** - Owen's dad from TV Glow who watches TV
#### Create Movie Post
Create a new movie post from IMDB ID:
```bash
# From IMDB ID
python3 scripts/new_movie.py tt1234567
# From IMDB URL
python3 scripts/new_movie.py https://www.imdb.com/title/tt1234567/
```
#### Import from Letterboxd
Import movies from your Letterboxd diary:
```bash
# Interactive mode - pick from recent
python3 scripts/import_letterboxd.py
# Import most recent entry
python3 scripts/import_letterboxd.py --latest
# Just list recent entries
python3 scripts/import_letterboxd.py --list
# Skip to theater/home questions
python3 scripts/import_letterboxd.py --theater
python3 scripts/import_letterboxd.py --home
```
#### Update Movie Metadata
Fetch and update movie metadata (poster, director, runtime, etc.):
```bash
# Update all posts with IMDB IDs
python3 scripts/fetch_movie_data.py
# Dry run (preview changes)
python3 scripts/fetch_movie_data.py --dry-run
# Force re-fetch even if data exists
python3 scripts/fetch_movie_data.py --force
```
### Beer Posts
#### Add Beer Call Entry
Add entries to the beer call yearly log:
```bash
# Interactive mode
python3 scripts/new_beercall.py
# Specific date
python3 scripts/new_beercall.py --date 2024-12-19
# List recent Untappd checkins
python3 scripts/new_beercall.py --list
```
## Environment Variables
### Ollama (for NFR automation)
```bash
# Ollama server (default: http://192.168.0.109:11434)
export OLLAMA_HOST=http://localhost:11434
# Model to use (default: llama3.2)
export OLLAMA_MODEL=llama3.2
```
## Common Workflows
### Creating an NFR Movie Post
```bash
# 1. Create the post
python3 scripts/new_nfr.py "Beverly Hills Cop"
# 2. Update metadata (director, runtime, etc.)
python3 scripts/fetch_movie_data.py
# 3. Edit the post
# - Add viewing details (format, rating)
# - Write your thoughts
# - Add Letterboxd URL
# 4. Build and preview
hugo server -D
# 5. Publish (remove draft: true)
```
### Importing Theater Movie from Letterboxd
```bash
# 1. Import from Letterboxd
python3 scripts/import_letterboxd.py --theater
# 2. Script will:
# - Fetch recent Letterboxd entries
# - Let you pick one
# - Ask for theater details (venue, time, crew, etc.)
# - Download poster from TMDB
# - Create draft post
# 3. Edit and publish
```
### Setting Up a New NFR Year
```bash
# 1. Find the LOC announcement URL for the year
# Example: https://newsroom.loc.gov/news/...
# 2. Run setup script (with ollama for best results)
python3 scripts/setup_nfr.py 2023 --url "https://newsroom.loc.gov/..."
# 3. Review generated file
cat scripts/nfr_data/nfr_2023.py
# 4. Integrate into new_nfr.py
# (Copy the dictionary into the main script)
# 5. Start creating posts!
python3 scripts/new_nfr.py "Terminator 2" --nfr-year 2023
```
## Troubleshooting
### "Module not found" errors
```bash
# Make sure venv is activated
source .venv/bin/activate
# Install/reinstall dependencies
pip install -r requirements.txt
```
### "TMDB API key" errors
```bash
# Check config exists
ls scripts/config.py
# If not, copy example and edit
cp scripts/config.example.py scripts/config.py
# Then add your API key
```
### Ollama connection errors
```bash
# Test ollama server
curl http://192.168.0.109:11434/api/tags
# Use --no-ollama flag to skip
python3 scripts/setup_nfr.py 2023 --no-ollama
```
## Documentation
- **NFR Automation**: `scripts/NFR_AUTOMATION.md` - Detailed guide for NFR automation with ollama
- **Config Example**: `scripts/config.example.py` - Template for API keys
## Directory Structure
```
scripts/
├── README.md # This file
├── NFR_AUTOMATION.md # NFR automation guide
├── config.example.py # Config template
├── config.py # Your config (gitignored)
├── nfr_data/ # Generated NFR data
│ ├── nfr_2023.py
│ └── nfr_2024.py
├── venues.json # Beer venue database
├── setup_nfr.py # Setup NFR year data
├── new_nfr.py # Create NFR movie post
├── new_movie.py # Create movie post
├── import_letterboxd.py # Import from Letterboxd
├── fetch_movie_data.py # Update movie metadata
├── new_beercall.py # Add beer call entry
└── new_techpost.py # Create tech post
```
## Getting Help
- Check the specific script's `--help`: `python3 scripts/new_nfr.py --help`
- Read `NFR_AUTOMATION.md` for NFR details
- Check error messages - they usually point to the issue
+438
View File
@@ -0,0 +1,438 @@
#!/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()
+2
View File
@@ -0,0 +1,2 @@
# This directory holds generated NFR data files
# Files are created by scripts/setup_nfr.py
+397
View File
@@ -0,0 +1,397 @@
#!/usr/bin/env python3
"""
Setup NFR (National Film Registry) data for a specific year.
This script fetches the Library of Congress announcement for a given year,
extracts film titles and descriptions, and generates a Python dictionary
that can be added to new_nfr.py.
Usage:
python3 scripts/setup_nfr.py 2024
python3 scripts/setup_nfr.py 2015 --output scripts/nfr_data/nfr_2015.py
python3 scripts/setup_nfr.py 2023 --no-ollama # Don't use ollama for extraction
Requirements:
- requests library
- access to ollama server (optional, for better extraction)
The script will:
1. Search for the LOC announcement URL for the given year
2. Fetch the announcement page
3. Use ollama (if available) or basic parsing to extract film data
4. Generate a Python dictionary with film titles, years, and descriptions
5. Save to a file or print to stdout
"""
import argparse
import json
import os
import re
import sys
from pathlib import Path
from urllib.parse import urljoin
import requests
# Configuration
SCRIPT_DIR = Path(__file__).parent
PROJECT_ROOT = SCRIPT_DIR.parent
NFR_DATA_DIR = SCRIPT_DIR / "nfr_data"
# Ollama configuration
OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "http://192.168.0.109:11434")
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2") # or whatever model you have
def search_for_nfr_announcement(year):
"""
Search for the LOC NFR announcement URL for a given year.
Returns dict with:
- newsroom_url: Main press release
- blog_url: Blog announcement (if found)
"""
print(f"Searching for {year} NFR announcement...")
# Try common URL patterns for LOC announcements
urls_to_try = []
# Newsroom pattern (most reliable for recent years)
# Example: https://newsroom.loc.gov/news/25-films-named-to-national-film-registry-for-preservation/
# The URL doesn't always have the year in it, so we'll search
# Try searching via web
search_queries = [
f"site:newsroom.loc.gov national film registry {year}",
f"site:blogs.loc.gov national film registry {year}",
f'"national film registry" {year} site:loc.gov'
]
results = {
"newsroom_url": None,
"blog_url": None,
"webcast_url": None,
}
# For now, return known patterns - user can manually find URL
# We'll enhance this with actual search later
print(f"\nPlease find the LOC announcement URL for {year}.")
print(f"\nCommon places to look:")
print(f" - https://newsroom.loc.gov/")
print(f" - https://blogs.loc.gov/now-see-hear/")
print(f" - https://www.loc.gov/programs/national-film-preservation-board/film-registry/")
# For 2024, we know the URL
if year == 2024:
results["newsroom_url"] = "https://newsroom.loc.gov/news/25-films-named-to-national-film-registry-for-preservation/s/55d5285d-916f-4105-b7d4-7fc3ba8664e3"
results["blog_url"] = "https://blogs.loc.gov/now-see-hear/2024/12/announcing-the-2024-national-film-registry/"
return results
# Prompt user for URL
url = input(f"\nEnter the LOC announcement URL for {year} (or press Enter to skip): ").strip()
if url:
results["newsroom_url"] = url
return results
def fetch_url_content(url):
"""Fetch content from a URL."""
print(f"Fetching {url}...")
resp = requests.get(url, timeout=30)
resp.raise_for_status()
return resp.text
def call_ollama(prompt, model=OLLAMA_MODEL, system_prompt=None):
"""
Call ollama API to process text.
Args:
prompt: The user prompt
model: Model name (default from OLLAMA_MODEL env var)
system_prompt: Optional system prompt
Returns:
The model's response text
"""
url = f"{OLLAMA_HOST}/api/generate"
payload = {
"model": model,
"prompt": prompt,
"stream": False,
}
if system_prompt:
payload["system"] = system_prompt
print(f"Calling ollama at {OLLAMA_HOST} with model {model}...")
try:
resp = requests.post(url, json=payload, timeout=300) # 5 min timeout
resp.raise_for_status()
data = resp.json()
return data.get("response", "")
except requests.exceptions.RequestException as e:
print(f"Error calling ollama: {e}")
return None
def extract_films_with_ollama(html_content, year):
"""
Use ollama to extract film data from HTML content.
Returns a list of dicts with: title, year, description
"""
system_prompt = """You are a helpful assistant that extracts structured data from web pages.
Your task is to extract information about films from National Film Registry announcements.
Output ONLY valid JSON, nothing else. No markdown formatting, no code blocks, just raw JSON."""
user_prompt = f"""From the following HTML content, extract ALL films that were added to the National Film Registry in {year}.
For each film, extract:
1. The exact title
2. The release year of the film
3. The description/reason why it was selected for preservation
Format your response as a JSON array of objects with this structure:
[
{{
"title": "Film Title",
"year": 1999,
"description": "The reason it was selected..."
}}
]
IMPORTANT:
- Extract ALL {year} films, typically 25 films
- Keep descriptions concise but complete
- Use the exact text from the announcement
- Output ONLY the JSON array, no other text
- Do not include markdown code blocks
HTML Content:
{html_content[:50000]}
""" # Limit to first 50k chars to avoid token limits
response = call_ollama(user_prompt, system_prompt=system_prompt)
if not response:
return None
# Try to parse JSON from response
try:
# Sometimes models wrap in code blocks, try to extract
json_match = re.search(r'(\[.*\])', response, re.DOTALL)
if json_match:
response = json_match.group(1)
films = json.loads(response)
return films
except json.JSONDecodeError as e:
print(f"Failed to parse JSON from ollama response: {e}")
print(f"Response was: {response[:500]}...")
return None
def extract_films_basic(html_content, year):
"""
Basic extraction without ollama - looks for common patterns.
This is a fallback method and may not work for all years.
"""
print("Using basic extraction (without ollama)...")
print("Note: This may not capture all details. Consider using --ollama for better results.")
films = []
# Look for numbered lists or bold film titles
# This is a simple heuristic and may need adjustment
# Pattern: Look for year in parentheses near potential titles
# Example: "Film Title (1999)"
pattern = r'([A-Z][^(]{3,50})\s*\((\d{4})\)'
matches = re.findall(pattern, html_content)
seen_titles = set()
for title, film_year in matches:
title = title.strip()
# Filter out obviously wrong matches
if title and len(title) > 3 and title not in seen_titles:
# Try to get a reasonable year range
try:
y = int(film_year)
if 1890 <= y <= year: # Reasonable film year range
films.append({
"title": title,
"year": y,
"description": "[Description not extracted - please add manually]"
})
seen_titles.add(title)
except ValueError:
pass
return films if films else None
def generate_python_dict(films, year):
"""
Generate Python code for the NFR dictionary.
Args:
films: List of film dicts
year: NFR induction year
Returns:
String containing Python code
"""
output = f'''# {year} National Film Registry inductees with LOC descriptions
# Source: [Add URL here]
NFR_{year} = {{'''
for film in films:
title = film["title"].replace("'", "\\'")
desc = film["description"].replace("'", "\\'").replace("\n", " ")
output += f'''
"{title}": {{
"year": {film["year"]},
"description": '{desc}'
}},'''
output += "\n}\n"
return output
def save_nfr_data(films, year, output_path=None):
"""
Save NFR data to a file.
Args:
films: List of film dicts
year: NFR induction year
output_path: Optional path to save to (default: nfr_data/nfr_YEAR.py)
"""
if output_path is None:
NFR_DATA_DIR.mkdir(exist_ok=True)
output_path = NFR_DATA_DIR / f"nfr_{year}.py"
else:
output_path = Path(output_path)
code = generate_python_dict(films, year)
output_path.write_text(code)
print(f"\n✓ Saved to {output_path}")
print(f"\nTo use this data:")
print(f" 1. Review and edit {output_path} if needed")
print(f" 2. Copy the NFR_{year} dictionary into scripts/new_nfr.py")
print(f" 3. Update the script to handle multiple years")
return output_path
def main():
parser = argparse.ArgumentParser(
description="Setup NFR data for a specific year"
)
parser.add_argument("year", type=int, help="NFR induction year (e.g., 2024)")
parser.add_argument(
"--url",
help="Direct URL to LOC announcement (skip search)"
)
parser.add_argument(
"--output",
help="Output file path (default: scripts/nfr_data/nfr_YEAR.py)"
)
parser.add_argument(
"--no-ollama",
action="store_true",
help="Don't use ollama for extraction (use basic parsing)"
)
parser.add_argument(
"--ollama-host",
default=OLLAMA_HOST,
help=f"Ollama server URL (default: {OLLAMA_HOST})"
)
parser.add_argument(
"--ollama-model",
default=OLLAMA_MODEL,
help=f"Ollama model to use (default: {OLLAMA_MODEL})"
)
args = parser.parse_args()
# Update ollama config from args
global OLLAMA_HOST, OLLAMA_MODEL
OLLAMA_HOST = args.ollama_host
OLLAMA_MODEL = args.ollama_model
print(f"\n{'='*60}")
print(f"Setting up NFR data for {args.year}")
print(f"{'='*60}\n")
# Get announcement URL
if args.url:
urls = {"newsroom_url": args.url}
else:
urls = search_for_nfr_announcement(args.year)
if not urls.get("newsroom_url"):
print("\nError: No announcement URL found.")
print("Please provide a URL with --url")
sys.exit(1)
# Fetch content
try:
html_content = fetch_url_content(urls["newsroom_url"])
except Exception as e:
print(f"Error fetching URL: {e}")
sys.exit(1)
# Extract films
films = None
if not args.no_ollama:
try:
films = extract_films_with_ollama(html_content, args.year)
except Exception as e:
print(f"Error using ollama: {e}")
print("Falling back to basic extraction...")
if not films:
films = extract_films_basic(html_content, args.year)
if not films:
print("\nError: Could not extract films from announcement.")
print("Try:")
print(" 1. Using --ollama if you skipped it")
print(" 2. Manually creating the dictionary")
sys.exit(1)
print(f"\n✓ Extracted {len(films)} films")
# Show preview
print("\nPreview of extracted films:")
for i, film in enumerate(films[:5], 1):
print(f" {i}. {film['title']} ({film['year']})")
if len(film['description']) > 100:
print(f" {film['description'][:100]}...")
else:
print(f" {film['description']}")
if len(films) > 5:
print(f" ... and {len(films) - 5} more")
# Confirm
confirm = input("\nSave this data? (Y/n): ").strip().lower()
if confirm == 'n':
print("Cancelled")
sys.exit(0)
# Save
output_path = save_nfr_data(films, args.year, args.output)
print("\n✓ Done!")
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB