Trying to automate the movie review pages

This commit is contained in:
mnw
2025-12-24 17:14:39 -06:00
parent a5782a3357
commit b09b638dca
11 changed files with 1053 additions and 8 deletions
+30
View File
@@ -0,0 +1,30 @@
# Build output
public/
resources/_gen/
# Hugo lock file
.hugo_build.lock
# Editor/IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Environment/secrets
.env
*.env
# Python
__pycache__/
*.pyc
.venv/
venv/
# Claude Code
.claude/
+7
View File
@@ -4,7 +4,14 @@ date: {{ .Date }}
draft: true
series: "Frank's Couch"
summary: ""
# 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: []
tags:
- gucci
- ghost theater
+40
View File
@@ -0,0 +1,40 @@
---
title: 'Lethal Tender'
date: 2025-12-24T22:47:20Z
draft: true
series: "Frank's Couch"
summary: ""
imdb: "tt0119520"
poster: "/images/posters/lethal-tender.jpg"
year: 1997
runtime: 94
director: "John Bradshaw"
genres:
- Action
- Thriller
tags:
- gucci
- ghost theater
- marcel
- amc-south
- amc-lakeline
- anticipated
- no-expectations
- had pizza
---
{{< imdbposter >}}
| Date watched | |
|---------------------|-------------------|
| Show Time | |
| Theater | |
| Theater Number | |
| Pizza | |
| Tickets | |
| Letterboxd Rating | |
| Crew | |
{{< /imdbposter >}}
Write your review here...
+1 -3
View File
@@ -5,15 +5,13 @@ languageCode: en
DefaultContentLanguage: en
theme: poison
publishDir: /sdf/arpa/gm/m/mnw/html
preserveTaxonomyNames: true
paginate: 10
favicon: favicon.ico
pluralizelisttitles: false
pluralizeListTitles: false
params:
brand: Double Lunch Dispatch
#remote_brand_image: https://mnw.sdf.org/blog/images/circular-me.png
brand_image: "/images/circular-me-250.png"
description: There's still time to blog ...
dark_mode: true
description: "A blog about watching movies, drinking beers, and using technology."
keywords: "blog, movies, beers, technology, life experiences"
+34 -5
View File
@@ -1,17 +1,46 @@
{{- $imdb := .Page.Params.imdb -}}
{{- $poster := printf "https://img.omdbapi.com/?i=%s&apikey=d9641e70" $imdb -}}
{{- $localPoster := .Page.Params.poster -}}
{{- $year := .Page.Params.year -}}
{{- $runtime := .Page.Params.runtime -}}
{{- $director := .Page.Params.director -}}
{{- $poster := "" -}}
{{- if $localPoster -}}
{{- $poster = $localPoster -}}
{{- else if $imdb -}}
{{- $poster = printf "https://img.omdbapi.com/?i=%s&apikey=d9641e70" $imdb -}}
{{- end -}}
<div style="display: flex; justify-content: flex-start; align-items: flex-start;">
<!-- Left section: code block with viewing details -->
<!-- Left section: viewing details -->
<div style="flex: 1; padding-right: 20px;">
{{ .Inner | markdownify }}
</div>
<!-- Right section: movie poster -->
<div style="flex-shrink: 0;">
<!-- Right section: movie poster and info -->
<div style="flex-shrink: 0; text-align: center;">
{{- if $imdb -}}
<a href="https://www.imdb.com/title/{{ $imdb }}" target="_blank">
<img src="{{ $poster }}" alt="Movie Poster" style="width: 200px;" />
</a>
{{- else if $poster -}}
<img src="{{ $poster }}" alt="Movie Poster" style="width: 200px;" />
{{- end -}}
{{- if or $year $runtime $director -}}
<div style="font-size: 0.85em; color: #666; margin-top: 8px;">
{{- if $year -}}<div>{{ $year }}</div>{{- end -}}
{{- if $runtime -}}<div>{{ $runtime }} min</div>{{- end -}}
{{- if $director -}}
<div>
{{- if reflect.IsSlice $director -}}
{{ delimit $director ", " }}
{{- else -}}
{{ $director }}
{{- end -}}
</div>
{{- end -}}
</div>
{{- end -}}
</div>
</div>
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# Build script for marcus-web
# Fetches movie data, then builds Hugo site
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_ROOT"
echo "=== Fetching movie data ==="
python3 scripts/fetch_movie_data.py
echo ""
echo "=== Building Hugo site ==="
hugo "$@"
echo ""
echo "=== Done ==="
+271
View File
@@ -0,0 +1,271 @@
#!/usr/bin/env python3
"""
Fetch movie data for Hugo posts based on IMDB ID in frontmatter.
Scans all posts with an `imdb` field and fetches missing data:
- Poster (downloaded locally)
- Runtime
- Year
- Director
- Genres
Usage:
python scripts/fetch_movie_data.py # Process all movie posts
python scripts/fetch_movie_data.py --dry-run # Show what would be updated
python scripts/fetch_movie_data.py --force # Re-fetch even if data exists
"""
import argparse
import os
import re
import sys
from pathlib import Path
import requests
import yaml
# Configuration
TMDB_API_KEY = "9e36fdcf70a4918e0dce327acc437dff"
# Paths
SCRIPT_DIR = Path(__file__).parent
PROJECT_ROOT = SCRIPT_DIR.parent
CONTENT_DIR = PROJECT_ROOT / "content" / "posts"
IMAGES_DIR = PROJECT_ROOT / "static" / "images" / "posters"
# Regex to split frontmatter from content
FRONTMATTER_RE = re.compile(r'^---\s*\n(.*?)\n---\s*\n', re.DOTALL)
def find_movie_by_imdb(imdb_id):
"""Find TMDB movie by IMDB 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 results:
return results[0]
return None
def get_movie_details(tmdb_id):
"""Get full movie details from TMDB."""
url = f"https://api.themoviedb.org/3/movie/{tmdb_id}"
params = {
"api_key": TMDB_API_KEY,
"append_to_response": "credits"
}
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
return resp.json()
def get_directors(credits):
"""Extract director names from credits."""
crew = credits.get("crew", [])
directors = [p["name"] for p in crew if p.get("job") == "Director"]
return directors
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 download_poster(poster_path, filename):
"""Download poster from TMDB."""
if not poster_path:
return None
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)
return f"/images/posters/{filename}"
def parse_post(filepath):
"""Parse a markdown post into frontmatter dict and content string."""
text = filepath.read_text()
match = FRONTMATTER_RE.match(text)
if not match:
return None, text
fm_text = match.group(1)
content = text[match.end():]
try:
frontmatter = yaml.safe_load(fm_text)
except yaml.YAMLError:
return None, text
return frontmatter, content
def write_post(filepath, frontmatter, content):
"""Write frontmatter and content back to markdown file."""
# Use default_flow_style=False for readable YAML
# Use allow_unicode=True for proper character handling
fm_text = yaml.dump(
frontmatter,
default_flow_style=False,
allow_unicode=True,
sort_keys=False
)
text = f"---\n{fm_text}---\n{content}"
filepath.write_text(text)
def process_post(filepath, dry_run=False, force=False):
"""Process a single post, fetching missing movie data."""
frontmatter, content = parse_post(filepath)
if frontmatter is None:
return False
imdb_id = frontmatter.get("imdb")
if not imdb_id:
return False
# Check what's missing
has_poster = bool(frontmatter.get("poster"))
has_runtime = bool(frontmatter.get("runtime"))
has_year = bool(frontmatter.get("year"))
has_director = bool(frontmatter.get("director"))
needs_update = not (has_poster and has_runtime and has_year and has_director)
if not needs_update and not force:
return False
print(f"\nProcessing: {filepath.name}")
print(f" IMDB: {imdb_id}")
if dry_run:
missing = []
if not has_poster:
missing.append("poster")
if not has_runtime:
missing.append("runtime")
if not has_year:
missing.append("year")
if not has_director:
missing.append("director")
print(f" Would fetch: {', '.join(missing)}")
return True
# Find movie on TMDB
print(" Finding movie on TMDB...")
movie = find_movie_by_imdb(imdb_id)
if not movie:
print(f" ERROR: Movie not found for IMDB ID: {imdb_id}")
return False
tmdb_id = movie["id"]
print(f" Found: {movie.get('title')} (TMDB: {tmdb_id})")
# Get full details
print(" Fetching details...")
details = get_movie_details(tmdb_id)
updated = False
# Update poster
if not has_poster or force:
poster_path = details.get("poster_path")
if poster_path:
title = frontmatter.get("title", "movie")
filename = f"{slugify(title)}.jpg"
print(f" Downloading poster...")
poster_url = download_poster(poster_path, filename)
if poster_url:
frontmatter["poster"] = poster_url
print(f" Poster saved: {poster_url}")
updated = True
# Update runtime
if not has_runtime or force:
runtime = details.get("runtime")
if runtime:
frontmatter["runtime"] = runtime
print(f" Runtime: {runtime} minutes")
updated = True
# Update year
if not has_year or force:
release_date = details.get("release_date", "")
if release_date:
year = release_date.split("-")[0]
frontmatter["year"] = int(year)
print(f" Year: {year}")
updated = True
# Update director
if not has_director or force:
credits = details.get("credits", {})
directors = get_directors(credits)
if directors:
# Store as string if single, list if multiple
if len(directors) == 1:
frontmatter["director"] = directors[0]
else:
frontmatter["director"] = directors
print(f" Director: {', '.join(directors)}")
updated = True
# Update genres (bonus)
if "genres" not in frontmatter or force:
genres = [g["name"] for g in details.get("genres", [])]
if genres:
frontmatter["genres"] = genres
updated = True
if updated:
write_post(filepath, frontmatter, content)
print(" Updated!")
return updated
def main():
parser = argparse.ArgumentParser(description="Fetch movie data for Hugo posts")
parser.add_argument("--dry-run", action="store_true", help="Show what would be updated")
parser.add_argument("--force", action="store_true", help="Re-fetch even if data exists")
parser.add_argument("file", nargs="?", help="Specific file to process")
args = parser.parse_args()
if args.file:
filepath = Path(args.file)
if not filepath.is_absolute():
filepath = PROJECT_ROOT / filepath
if not filepath.exists():
print(f"File not found: {filepath}")
sys.exit(1)
files = [filepath]
else:
files = list(CONTENT_DIR.glob("**/*.md"))
print(f"Scanning {len(files)} posts for movie data...")
updated = 0
for filepath in files:
if process_post(filepath, dry_run=args.dry_run, force=args.force):
updated += 1
print(f"\n{'Would update' if args.dry_run else 'Updated'}: {updated} posts")
if __name__ == "__main__":
main()
+285
View File
@@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""
Import movies from Letterboxd diary to Hugo draft posts.
Usage:
python scripts/import_letterboxd.py # Interactive mode - pick from recent
python scripts/import_letterboxd.py --latest # Import most recent entry
python scripts/import_letterboxd.py --list # Just list recent entries
"""
import argparse
import os
import re
import sys
from datetime import datetime
from pathlib import Path
from urllib.parse import urlparse
import xml.etree.ElementTree as ET
import requests
# Configuration
LETTERBOXD_USER = "marcusEID"
TMDB_API_KEY = "9e36fdcf70a4918e0dce327acc437dff" # From get_posters.py
RSS_URL = f"https://letterboxd.com/{LETTERBOXD_USER}/rss/"
# 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"
# XML namespaces in Letterboxd RSS
NAMESPACES = {
"letterboxd": "https://letterboxd.com",
"tmdb": "https://themoviedb.org",
"dc": "http://purl.org/dc/elements/1.1/",
}
def fetch_rss():
"""Fetch and parse Letterboxd RSS feed."""
resp = requests.get(RSS_URL, timeout=10)
resp.raise_for_status()
return ET.fromstring(resp.content)
def parse_movies(root):
"""Extract movie entries from RSS (skip lists)."""
movies = []
for item in root.findall(".//item"):
# Skip lists (they don't have tmdb:movieId)
tmdb_id = item.find("tmdb:movieId", NAMESPACES)
if tmdb_id is None:
continue
title = item.find("letterboxd:filmTitle", NAMESPACES)
year = item.find("letterboxd:filmYear", NAMESPACES)
rating = item.find("letterboxd:memberRating", NAMESPACES)
watched = item.find("letterboxd:watchedDate", NAMESPACES)
rewatch = item.find("letterboxd:rewatch", NAMESPACES)
link = item.find("link")
movies.append({
"tmdb_id": tmdb_id.text,
"title": title.text if title is not None else "Unknown",
"year": year.text if year is not None else "",
"rating": rating.text if rating is not None else "",
"watched_date": watched.text if watched is not None else "",
"rewatch": rewatch.text if rewatch is not None else "No",
"letterboxd_url": link.text if link is not None else "",
})
return movies
def get_tmdb_details(tmdb_id):
"""Fetch movie details from TMDB including IMDB ID and poster."""
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()
data = resp.json()
return {
"imdb_id": data.get("imdb_id", ""),
"poster_path": data.get("poster_path", ""),
"overview": data.get("overview", ""),
}
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 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 rating_to_stars(rating):
"""Convert numeric rating to star display."""
if not rating:
return ""
r = float(rating)
full = int(r)
half = r - full >= 0.5
stars = "*" * full
if half:
stars += " 1/2"
return f"{stars} ({rating})"
def create_draft_post(movie, tmdb_details, poster_url):
"""Create a Hugo draft post for the movie."""
slug = slugify(movie["title"])
filename = f"{slug}.md"
filepath = CONTENT_DIR / filename
if filepath.exists():
print(f" Post already exists: {filepath.relative_to(PROJECT_ROOT)}")
return None
# Format the date for Hugo
now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
# Format watched date nicely
watched = movie["watched_date"]
if watched:
try:
dt = datetime.strptime(watched, "%Y-%m-%d")
watched_display = dt.strftime("%B %d, %Y")
except ValueError:
watched_display = watched
else:
watched_display = ""
imdb_id = tmdb_details.get("imdb_id", "")
rating_display = rating_to_stars(movie["rating"])
# Build the frontmatter and content
content = f'''---
title: '{movie["title"]}'
date: {now}
draft: true
series: "Frank's Couch"
summary: ""
imdb: "{imdb_id}"
poster: "{poster_url or ''}"
tags:
- gucci
- ghost theater
- marcel
- amc-south
- amc-lakeline
- anticipated
- no-expectations
- had pizza
---
{{{{< imdbposter >}}}}
| Date watched | {watched_display} |
|---------------------|-------------------|
| Show Time | |
| Theater | |
| Theater Number | |
| Pizza | |
| Tickets | |
| Letterboxd Rating | {rating_display} |
| Crew | |
{{{{< /imdbposter >}}}}
Write your review here...
'''
filepath.write_text(content)
print(f" Draft created: {filepath.relative_to(PROJECT_ROOT)}")
return filepath
def display_movies(movies, limit=10):
"""Display a list of recent movies."""
print(f"\nRecent movies from Letterboxd ({LETTERBOXD_USER}):\n")
for i, m in enumerate(movies[:limit], 1):
rewatch = " (rewatch)" if m["rewatch"] == "Yes" else ""
rating = f" - {m['rating']}*" if m["rating"] else ""
print(f" {i}. {m['title']} ({m['year']}){rating}{rewatch}")
print(f" Watched: {m['watched_date']}")
print()
def import_movie(movie):
"""Import a single movie: fetch details, download poster, create post."""
print(f"\nImporting: {movie['title']} ({movie['year']})")
# Get TMDB details
print(" Fetching TMDB details...")
tmdb = get_tmdb_details(movie["tmdb_id"])
# Download poster
poster_url = None
if tmdb["poster_path"]:
print(" Downloading poster...")
poster_filename = f"{slugify(movie['title'])}.jpg"
poster_url = download_poster(tmdb["poster_path"], poster_filename)
# Create draft post
print(" Creating draft post...")
filepath = create_draft_post(movie, tmdb, poster_url)
if filepath:
print(f"\nDone! Edit your draft at: {filepath.relative_to(PROJECT_ROOT)}")
if tmdb.get("imdb_id"):
print(f"IMDB: https://www.imdb.com/title/{tmdb['imdb_id']}/")
return filepath
def main():
parser = argparse.ArgumentParser(description="Import Letterboxd movies to Hugo")
parser.add_argument("--latest", action="store_true", help="Import most recent entry")
parser.add_argument("--list", action="store_true", help="Just list recent entries")
parser.add_argument("--count", type=int, default=10, help="Number of entries to show")
args = parser.parse_args()
print("Fetching Letterboxd RSS feed...")
try:
root = fetch_rss()
except Exception as e:
print(f"Error fetching RSS: {e}")
sys.exit(1)
movies = parse_movies(root)
if not movies:
print("No movies found in feed.")
sys.exit(1)
if args.list:
display_movies(movies, args.count)
sys.exit(0)
if args.latest:
import_movie(movies[0])
sys.exit(0)
# Interactive mode
display_movies(movies, args.count)
try:
choice = input("Enter number to import (or 'q' to quit): ").strip()
if choice.lower() == 'q':
sys.exit(0)
idx = int(choice) - 1
if 0 <= idx < len(movies):
import_movie(movies[idx])
else:
print("Invalid selection")
sys.exit(1)
except (ValueError, KeyboardInterrupt):
print("\nCancelled")
sys.exit(1)
if __name__ == "__main__":
main()
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""
Create a new movie post from an IMDB ID.
Usage:
python scripts/new_movie.py tt1234567
python scripts/new_movie.py tt1234567 --title "Custom Title"
python scripts/new_movie.py https://www.imdb.com/title/tt1234567/
This fetches all metadata (title, year, runtime, director, poster) and creates
a ready-to-edit draft post.
"""
import argparse
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
import requests
# Configuration
TMDB_API_KEY = "9e36fdcf70a4918e0dce327acc437dff"
# Paths
SCRIPT_DIR = Path(__file__).parent
PROJECT_ROOT = SCRIPT_DIR.parent
CONTENT_DIR = PROJECT_ROOT / "content" / "posts"
IMAGES_DIR = PROJECT_ROOT / "static" / "images" / "posters"
def extract_imdb_id(input_str):
"""Extract IMDB ID from URL or raw ID."""
match = re.search(r'(tt\d+)', input_str)
if match:
return match.group(1)
return None
def find_movie_by_imdb(imdb_id):
"""Find TMDB movie by IMDB 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 results:
return results[0]
return None
def get_movie_details(tmdb_id):
"""Get full movie details from TMDB."""
url = f"https://api.themoviedb.org/3/movie/{tmdb_id}"
params = {
"api_key": TMDB_API_KEY,
"append_to_response": "credits"
}
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
return resp.json()
def get_directors(credits):
"""Extract director names from credits."""
crew = credits.get("crew", [])
directors = [p["name"] for p in crew if p.get("job") == "Director"]
return directors
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 download_poster(poster_path, filename):
"""Download and save poster. Returns local path."""
if not poster_path:
return ""
# w500 = 500px wide, good for 200px display on retina
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)
return f"/images/posters/{filename}"
def create_post(imdb_id, details, poster_path, custom_title=None):
"""Create the Hugo post with all metadata."""
title = custom_title or details.get("title", "Untitled")
slug = slugify(title)
filename = f"{slug}.md"
filepath = CONTENT_DIR / filename
if filepath.exists():
print(f"Post already exists: {filepath}")
return None
# Download poster
poster_url = ""
if details.get("poster_path"):
poster_filename = f"{slug}.jpg"
print(f"Downloading poster...")
poster_url = download_poster(details["poster_path"], poster_filename)
# Extract metadata
release_date = details.get("release_date", "")
year = int(release_date.split("-")[0]) if release_date else ""
runtime = details.get("runtime", "")
credits = details.get("credits", {})
directors = get_directors(credits)
director_str = directors[0] if len(directors) == 1 else directors if directors else ""
genres = [g["name"] for g in details.get("genres", [])]
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# Build frontmatter
# Using manual formatting to control output
genres_yaml = "\n".join(f" - {g}" for g in genres) if genres else "[]"
if isinstance(director_str, list):
director_yaml = "\n".join(f" - {d}" for d in director_str)
director_yaml = f"\n{director_yaml}"
else:
director_yaml = f' "{director_str}"' if director_str else ' ""'
content = f'''---
title: '{title}'
date: {now}
draft: true
series: "Frank's Couch"
summary: ""
imdb: "{imdb_id}"
poster: "{poster_url}"
year: {year}
runtime: {runtime}
director:{director_yaml}
genres:
{genres_yaml}
tags:
- gucci
- ghost theater
- marcel
- amc-south
- amc-lakeline
- anticipated
- no-expectations
- had pizza
---
{{{{< imdbposter >}}}}
| Date watched | |
|---------------------|-------------------|
| Show Time | |
| Theater | |
| Theater Number | |
| Pizza | |
| Tickets | |
| Letterboxd Rating | |
| Crew | |
{{{{< /imdbposter >}}}}
Write your review here...
'''
filepath.write_text(content)
return filepath
def main():
parser = argparse.ArgumentParser(
description="Create a new movie post from IMDB ID",
epilog="Example: python scripts/new_movie.py tt1234567"
)
parser.add_argument("imdb", help="IMDB ID or URL (e.g., tt1234567)")
parser.add_argument("--title", help="Custom title (default: from TMDB)")
args = parser.parse_args()
# Extract IMDB ID
imdb_id = extract_imdb_id(args.imdb)
if not imdb_id:
print(f"Invalid IMDB ID: {args.imdb}")
print("Expected format: tt1234567 or https://www.imdb.com/title/tt1234567/")
sys.exit(1)
print(f"Looking up {imdb_id}...")
# Find on TMDB
movie = find_movie_by_imdb(imdb_id)
if not movie:
print(f"Movie not found for IMDB ID: {imdb_id}")
sys.exit(1)
tmdb_id = movie["id"]
print(f"Found: {movie.get('title')} ({movie.get('release_date', '')[:4]})")
# Get full details
print("Fetching details...")
details = get_movie_details(tmdb_id)
# Create post
filepath = create_post(imdb_id, details, details.get("poster_path"), args.title)
if filepath:
print(f"\nCreated: {filepath.relative_to(PROJECT_ROOT)}")
print(f"IMDB: https://www.imdb.com/title/{imdb_id}/")
print(f"\nEdit your post, then run: ./scripts/build.sh")
if __name__ == "__main__":
main()
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
# Remote publish script for marcus-web
# Pushes local changes if needed, then builds on SDF
set -e
SDF_HOST="mnw@sdf.org"
REMOTE_DIR="~/marcus-web"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== Remote Publish ===${NC}"
echo ""
# Check if we're in a git repo
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo -e "${RED}Error: Not in a git repository${NC}"
exit 1
fi
# Fetch latest from remote to compare
echo "Checking repository status..."
git fetch origin 2>/dev/null || true
# Check for uncommitted changes (staged or unstaged)
if ! git diff-index --quiet HEAD -- 2>/dev/null || [ -n "$(git ls-files --others --exclude-standard)" ]; then
echo -e "${YELLOW}You have uncommitted changes:${NC}"
git status --short
echo ""
read -p "Would you like to commit these changes? (y/n) " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
# Stage all changes
git add -A
read -p "Custom commit message? (y/n) " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
read -p "Enter commit message: " COMMIT_MSG
else
# Try to find a post title from recently modified markdown files
COMMIT_MSG=""
RECENT_MD=$(git diff --cached --name-only | grep '\.md$' | head -1)
if [ -n "$RECENT_MD" ] && [ -f "$RECENT_MD" ]; then
# Extract title from frontmatter
TITLE=$(grep -m1 "^title:" "$RECENT_MD" 2>/dev/null | sed "s/^title:[[:space:]]*['\"]*//" | sed "s/['\"].*$//")
if [ -n "$TITLE" ]; then
COMMIT_MSG="Remote Publish Auto Commit - $TITLE"
fi
fi
# Fallback to date-based message
if [ -z "$COMMIT_MSG" ]; then
COMMIT_MSG="Auto Commit - $(date '+%Y-%m-%d %H:%M')"
fi
echo "Using commit message: $COMMIT_MSG"
fi
git commit -m "$COMMIT_MSG"
echo -e "${GREEN}Changes committed${NC}"
echo ""
else
echo -e "${YELLOW}Continuing without committing...${NC}"
read -p "Are you sure? Remote will not have these changes. (y/n) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
fi
fi
# Check if local is ahead of remote
LOCAL=$(git rev-parse HEAD 2>/dev/null)
REMOTE=$(git rev-parse @{u} 2>/dev/null || echo "")
BASE=$(git merge-base HEAD @{u} 2>/dev/null || echo "")
if [ -z "$REMOTE" ]; then
echo -e "${YELLOW}Warning: No upstream branch configured${NC}"
elif [ "$LOCAL" != "$REMOTE" ]; then
if [ "$LOCAL" = "$BASE" ]; then
echo -e "${YELLOW}Local is behind remote. You may want to pull first.${NC}"
elif [ "$REMOTE" = "$BASE" ]; then
echo -e "${YELLOW}Local commits not pushed to remote:${NC}"
git log --oneline @{u}..HEAD
echo ""
read -p "Push changes before publishing? (y/n) " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Pushing..."
git push
echo -e "${GREEN}Pushed successfully${NC}"
else
echo -e "${YELLOW}Skipping push. Remote will not have latest changes.${NC}"
read -p "Continue anyway? (y/n) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
fi
else
echo -e "${YELLOW}Warning: Local and remote have diverged${NC}"
read -p "Continue anyway? (y/n) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
fi
else
echo -e "${GREEN}Repository is up to date with remote${NC}"
fi
echo ""
echo -e "${GREEN}=== Connecting to SDF ===${NC}"
echo ""
# Build the remote command
REMOTE_CMD="cd $REMOTE_DIR && git pull && hugo && mkhomepage -p"
# Try SSH with key auth first, fall back to password prompt
if ssh -o BatchMode=yes -o ConnectTimeout=5 "$SDF_HOST" "echo 'SSH key auth successful'" 2>/dev/null; then
echo "Using SSH key authentication..."
ssh "$SDF_HOST" "$REMOTE_CMD"
else
echo "SSH key auth failed or not configured, using password..."
ssh "$SDF_HOST" "$REMOTE_CMD"
fi
echo ""
echo -e "${GREEN}=== Published successfully! ===${NC}"
echo "Site is live at: https://mnw.sdf.org/"
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB