diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8a8a69 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/archetypes/movie.md b/archetypes/movie.md index e8a794a..ab216c2 100644 --- a/archetypes/movie.md +++ b/archetypes/movie.md @@ -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 diff --git a/content/posts/lethal-tender.md b/content/posts/lethal-tender.md new file mode 100644 index 0000000..c5217a9 --- /dev/null +++ b/content/posts/lethal-tender.md @@ -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... + diff --git a/hugo.yml b/hugo.yml index 7f86fee..b8f6301 100644 --- a/hugo.yml +++ b/hugo.yml @@ -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" diff --git a/layouts/shortcodes/imdbposter.html b/layouts/shortcodes/imdbposter.html index 9ec5cd3..a0f7b75 100644 --- a/layouts/shortcodes/imdbposter.html +++ b/layouts/shortcodes/imdbposter.html @@ -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 -}}
- +
{{ .Inner | markdownify }}
- - -
+ + +
+ {{- if $imdb -}} Movie Poster + {{- else if $poster -}} + Movie Poster + {{- end -}} + {{- if or $year $runtime $director -}} +
+ {{- if $year -}}
{{ $year }}
{{- end -}} + {{- if $runtime -}}
{{ $runtime }} min
{{- end -}} + {{- if $director -}} +
+ {{- if reflect.IsSlice $director -}} + {{ delimit $director ", " }} + {{- else -}} + {{ $director }} + {{- end -}} +
+ {{- end -}} +
+ {{- end -}}
diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..a5df9f4 --- /dev/null +++ b/scripts/build.sh @@ -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 ===" diff --git a/scripts/fetch_movie_data.py b/scripts/fetch_movie_data.py new file mode 100755 index 0000000..f42c2e5 --- /dev/null +++ b/scripts/fetch_movie_data.py @@ -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() diff --git a/scripts/import_letterboxd.py b/scripts/import_letterboxd.py new file mode 100755 index 0000000..ddbf3da --- /dev/null +++ b/scripts/import_letterboxd.py @@ -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() diff --git a/scripts/new_movie.py b/scripts/new_movie.py new file mode 100755 index 0000000..6f17c66 --- /dev/null +++ b/scripts/new_movie.py @@ -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() diff --git a/scripts/remote_publish.sh b/scripts/remote_publish.sh new file mode 100755 index 0000000..816e790 --- /dev/null +++ b/scripts/remote_publish.sh @@ -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/" diff --git a/static/images/posters/lethal-tender.jpg b/static/images/posters/lethal-tender.jpg new file mode 100644 index 0000000..5a51e05 Binary files /dev/null and b/static/images/posters/lethal-tender.jpg differ