Finishing up the trio of scripts for the various post types.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# marcus-web
|
||||
|
||||
Personal blog powered by Hugo. Many references to I Saw The TV Glow its a fantastic movie.
|
||||
|
||||
## Clone
|
||||
|
||||
```bash
|
||||
git clone ssh://git@git.sdf.org/mnw/marcus-web.git
|
||||
cd marcus-web
|
||||
```
|
||||
|
||||
## Setup Scripts
|
||||
|
||||
```bash
|
||||
./scripts/setup.sh
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
See `scripts/execution-notes.txt` for full details, but the short version:
|
||||
|
||||
```bash
|
||||
# Movies (Frank's Couch)
|
||||
python scripts/import_letterboxd.py
|
||||
|
||||
# Beer Calls (Luna Juice)
|
||||
python scripts/new_beercall.py
|
||||
|
||||
# Beer Events
|
||||
python scripts/new_lunajuice.py
|
||||
|
||||
# Tech Posts (Fun Center)
|
||||
python scripts/new_techpost.py
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Hugo builds happen on SDF, but using this repo and the scripts I can work on it locally where I am.
|
||||
@@ -0,0 +1 @@
|
||||
requests
|
||||
@@ -0,0 +1,101 @@
|
||||
================================================================================
|
||||
MARCUS-WEB SCRIPTS CHEAT SHEET
|
||||
================================================================================
|
||||
|
||||
FIRST TIME SETUP
|
||||
----------------
|
||||
After cloning on a new machine:
|
||||
|
||||
cd marcus-web
|
||||
./scripts/setup.sh
|
||||
|
||||
This creates a .venv and installs dependencies (just 'requests').
|
||||
|
||||
Then either activate the venv:
|
||||
|
||||
source .venv/bin/activate
|
||||
python scripts/import_letterboxd.py
|
||||
|
||||
Or run directly:
|
||||
|
||||
.venv/bin/python scripts/import_letterboxd.py
|
||||
|
||||
|
||||
|
||||
MOVIES (Frank's Couch)
|
||||
----------------------
|
||||
Import a movie from Letterboxd:
|
||||
|
||||
python scripts/import_letterboxd.py
|
||||
|
||||
--latest Import most recent without picking
|
||||
--theater Skip straight to theater questions
|
||||
--home Skip straight to home video questions
|
||||
--list Just show recent movies, don't import
|
||||
|
||||
Workflow: Log movie on Letterboxd → Run script → Pick movie → Answer prompts
|
||||
Creates: content/posts/<movie-slug>.md + downloads poster
|
||||
|
||||
|
||||
BEER CALLS (Luna Juice - Weekly Thursday Meetups)
|
||||
-------------------------------------------------
|
||||
Add a beer call to the yearly log:
|
||||
|
||||
python scripts/new_beercall.py
|
||||
|
||||
--date 2024-12-30 Specific date (for Beer Crawl, holidays, etc)
|
||||
--list Just show recent Untappd checkins
|
||||
|
||||
Workflow: Run script → It checks Untappd for where you were → Pick venue → Done
|
||||
Appends to: content/posts/beercall/2024.md (or 2025.md, etc - auto-created)
|
||||
|
||||
|
||||
LUNA JUICE EVENTS (Festivals, Special Occasions)
|
||||
------------------------------------------------
|
||||
Create a standalone beer event post:
|
||||
|
||||
python scripts/new_lunajuice.py
|
||||
python scripts/new_lunajuice.py "Beer Festival 2025"
|
||||
|
||||
Creates: content/posts/beercall/<event-slug>.md
|
||||
|
||||
|
||||
VENUE DATABASE
|
||||
--------------
|
||||
Known venues are stored in: scripts/venues.json
|
||||
New venues are added automatically when you enter them.
|
||||
|
||||
|
||||
TECH POSTS (Fun Center)
|
||||
-----------------------
|
||||
Create a new technology blog post:
|
||||
|
||||
python scripts/new_techpost.py
|
||||
python scripts/new_techpost.py "My Post Title"
|
||||
|
||||
Prompts for:
|
||||
- Type: How I Did It / Grinds My Gears / Quick Tip
|
||||
- Tags: suggests common ones, you add more
|
||||
- Summary: one-liner
|
||||
|
||||
Creates a skeleton outline based on post type so you just fill in the blanks.
|
||||
|
||||
|
||||
MASTODON COMMENTS
|
||||
-----------------
|
||||
After publishing a post and tooting about it:
|
||||
|
||||
1. Get the Mastodon post ID from the URL (the number at the end)
|
||||
2. Add to your post's front matter:
|
||||
|
||||
mastodon_id: "123456789"
|
||||
|
||||
3. Rebuild site - comments will show with count
|
||||
|
||||
To block a reply:
|
||||
|
||||
mastodon_blocked:
|
||||
- "https://tilde.zone/@someone/123456789"
|
||||
|
||||
|
||||
================================================================================
|
||||
Executable
+347
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Add a new Beer Call entry to the yearly log.
|
||||
|
||||
Usage:
|
||||
python scripts/new_beercall.py # Interactive, defaults to last Thursday
|
||||
python scripts/new_beercall.py --date 2024-12-19 # Specific date
|
||||
python scripts/new_beercall.py --list # Just show recent Untappd checkins
|
||||
|
||||
Fetches recent checkins from Untappd RSS to help identify venue.
|
||||
Beer calls are typically on Thursdays, except for special events like
|
||||
Beer Crawl (around New Year's) or when holidays fall on Thursday.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
# Configuration
|
||||
UNTAPPD_RSS = "https://untappd.com/rss/user/Craniumslows?key=e8110a1087c289fdb992448e75adf35c"
|
||||
|
||||
# Paths
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||
BEERCALL_DIR = PROJECT_ROOT / "content" / "posts" / "beercall"
|
||||
VENUES_FILE = SCRIPT_DIR / "venues.json"
|
||||
|
||||
|
||||
def load_venues():
|
||||
"""Load venue database from JSON file."""
|
||||
if VENUES_FILE.exists():
|
||||
with open(VENUES_FILE) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def save_venues(venues):
|
||||
"""Save updated venue database."""
|
||||
with open(VENUES_FILE, "w") as f:
|
||||
json.dump(venues, f, indent=2)
|
||||
|
||||
|
||||
def fetch_untappd_rss():
|
||||
"""Fetch and parse Untappd RSS feed."""
|
||||
try:
|
||||
resp = requests.get(UNTAPPD_RSS, timeout=10)
|
||||
resp.raise_for_status()
|
||||
return ET.fromstring(resp.content)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not fetch Untappd RSS: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_checkins(root):
|
||||
"""Extract checkin data from RSS."""
|
||||
checkins = []
|
||||
if root is None:
|
||||
return checkins
|
||||
|
||||
for item in root.findall(".//item"):
|
||||
title = item.find("title")
|
||||
pub_date = item.find("pubDate")
|
||||
description = item.find("description")
|
||||
|
||||
if title is not None and pub_date is not None:
|
||||
# Parse title: "Cranium S. is drinking a Beer by Brewery at Venue"
|
||||
title_text = title.text or ""
|
||||
|
||||
# Extract venue (after " at ")
|
||||
venue_match = re.search(r" at (.+)$", title_text)
|
||||
venue = venue_match.group(1) if venue_match else ""
|
||||
|
||||
# Extract beer and brewery
|
||||
beer_match = re.search(r"is drinking an? (.+) by (.+?) at", title_text)
|
||||
if beer_match:
|
||||
beer = beer_match.group(1)
|
||||
brewery = beer_match.group(2)
|
||||
else:
|
||||
beer = ""
|
||||
brewery = ""
|
||||
|
||||
# Parse date
|
||||
try:
|
||||
dt = datetime.strptime(pub_date.text, "%a, %d %b %Y %H:%M:%S %z")
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
checkins.append({
|
||||
"date": dt,
|
||||
"venue": venue,
|
||||
"beer": beer,
|
||||
"brewery": brewery,
|
||||
"notes": description.text if description is not None else "",
|
||||
})
|
||||
|
||||
return checkins
|
||||
|
||||
|
||||
def get_last_thursday():
|
||||
"""Get the date of the most recent Thursday (including today if Thursday)."""
|
||||
today = datetime.now()
|
||||
days_since_thursday = (today.weekday() - 3) % 7
|
||||
if days_since_thursday == 0 and today.hour < 12:
|
||||
# If it's Thursday morning, probably mean last Thursday
|
||||
days_since_thursday = 7
|
||||
return today - timedelta(days=days_since_thursday)
|
||||
|
||||
|
||||
def find_venue_by_name(venues, name):
|
||||
"""Try to match a venue name to our database."""
|
||||
name_lower = name.lower()
|
||||
for key, venue in venues.items():
|
||||
if name_lower == venue["name"].lower():
|
||||
return key, venue
|
||||
for alias in venue.get("aliases", []):
|
||||
if name_lower == alias.lower() or alias.lower() in name_lower:
|
||||
return key, venue
|
||||
return None, None
|
||||
|
||||
|
||||
def display_venues(venues):
|
||||
"""Display numbered list of venues."""
|
||||
print("\nKnown venues:")
|
||||
sorted_venues = sorted(venues.items(), key=lambda x: x[1]["name"])
|
||||
for i, (key, venue) in enumerate(sorted_venues, 1):
|
||||
print(f" {i:2}. {venue['name']}")
|
||||
return sorted_venues
|
||||
|
||||
|
||||
def get_or_create_year_file(year):
|
||||
"""Get the path to the year's beer call log, creating if needed."""
|
||||
BEERCALL_DIR.mkdir(parents=True, exist_ok=True)
|
||||
filepath = BEERCALL_DIR / f"{year}.md"
|
||||
|
||||
if not filepath.exists():
|
||||
# Create new year file with frontmatter
|
||||
content = f"""+++
|
||||
title = 'Beer Call Log for {year}'
|
||||
date = {datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')}
|
||||
draft = false
|
||||
summary = 'A listing of the beer calls that I have remembered to write down in {year}'
|
||||
series = "Luna Juice"
|
||||
+++
|
||||
|
||||
"""
|
||||
filepath.write_text(content)
|
||||
print(f"Created new log file: {filepath.relative_to(PROJECT_ROOT)}")
|
||||
|
||||
return filepath
|
||||
|
||||
|
||||
def format_date_header(date):
|
||||
"""Format date for the entry header."""
|
||||
return date.strftime("%B %-d, %Y") # e.g., "December 19, 2024"
|
||||
|
||||
|
||||
def add_entry(filepath, venue_name, address, beerlist, date, attendees, notes):
|
||||
"""Add a new beer call entry to the log file."""
|
||||
# Read current content
|
||||
content = filepath.read_text()
|
||||
|
||||
# Find where to insert (after frontmatter, before first entry or at end)
|
||||
# We want newest entries at the top
|
||||
lines = content.split("\n")
|
||||
|
||||
# Find end of frontmatter
|
||||
frontmatter_end = 0
|
||||
in_frontmatter = False
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip() == "+++":
|
||||
if in_frontmatter:
|
||||
frontmatter_end = i + 1
|
||||
break
|
||||
else:
|
||||
in_frontmatter = True
|
||||
|
||||
# Build the new entry
|
||||
date_str = format_date_header(date)
|
||||
entry = f"""
|
||||
# {venue_name} - {date_str}
|
||||
| | |
|
||||
| :------------------- | :---------------- |
|
||||
| Location | {address} |
|
||||
| Beerlist | {beerlist} |
|
||||
| Attendees | {attendees} |
|
||||
| Notes | {notes} |
|
||||
|
||||
"""
|
||||
|
||||
# Insert after frontmatter (and any blank lines)
|
||||
insert_pos = frontmatter_end
|
||||
while insert_pos < len(lines) and lines[insert_pos].strip() == "":
|
||||
insert_pos += 1
|
||||
|
||||
# Insert the new entry
|
||||
new_lines = lines[:insert_pos] + entry.split("\n") + lines[insert_pos:]
|
||||
filepath.write_text("\n".join(new_lines))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Add a new Beer Call entry")
|
||||
parser.add_argument("--date", help="Date of beer call (YYYY-MM-DD), default is last Thursday")
|
||||
parser.add_argument("--list", action="store_true", help="Just list recent Untappd checkins")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load venues
|
||||
venues = load_venues()
|
||||
|
||||
# Determine target date
|
||||
if args.date:
|
||||
try:
|
||||
target_date = datetime.strptime(args.date, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
print("Invalid date format. Use YYYY-MM-DD")
|
||||
sys.exit(1)
|
||||
else:
|
||||
target_date = get_last_thursday()
|
||||
|
||||
print(f"Beer Call date: {target_date.strftime('%A, %B %-d, %Y')}")
|
||||
|
||||
# Fetch Untappd checkins
|
||||
print("\nFetching Untappd checkins...")
|
||||
root = fetch_untappd_rss()
|
||||
checkins = parse_checkins(root)
|
||||
|
||||
# Filter to target date
|
||||
target_date_str = target_date.strftime("%Y-%m-%d")
|
||||
day_checkins = [c for c in checkins if c["date"].strftime("%Y-%m-%d") == target_date_str]
|
||||
|
||||
if args.list:
|
||||
print(f"\nRecent Untappd checkins:")
|
||||
for c in checkins[:15]:
|
||||
print(f" {c['date'].strftime('%Y-%m-%d %H:%M')} - {c['beer']} at {c['venue']}")
|
||||
sys.exit(0)
|
||||
|
||||
# Show checkins for the target date
|
||||
if day_checkins:
|
||||
print(f"\nUntappd checkins on {target_date_str}:")
|
||||
venues_seen = set()
|
||||
for c in day_checkins:
|
||||
if c["venue"] not in venues_seen:
|
||||
print(f" - {c['venue']}: {c['beer']} by {c['brewery']}")
|
||||
venues_seen.add(c["venue"])
|
||||
|
||||
# Try to suggest a venue
|
||||
suggested_venue = None
|
||||
for c in day_checkins:
|
||||
key, venue = find_venue_by_name(venues, c["venue"])
|
||||
if venue:
|
||||
suggested_venue = (key, venue, c["venue"])
|
||||
break
|
||||
else:
|
||||
print(f"\nNo Untappd checkins found for {target_date_str}")
|
||||
suggested_venue = None
|
||||
|
||||
# Venue selection
|
||||
print("\n" + "=" * 50)
|
||||
if suggested_venue:
|
||||
key, venue, untappd_name = suggested_venue
|
||||
print(f"Suggested venue from Untappd: {venue['name']}")
|
||||
use_suggested = input("Use this venue? (Y/n): ").strip().lower()
|
||||
if use_suggested != "n":
|
||||
selected_venue = venue
|
||||
selected_key = key
|
||||
else:
|
||||
selected_venue = None
|
||||
else:
|
||||
selected_venue = None
|
||||
|
||||
if not selected_venue:
|
||||
sorted_venues = display_venues(venues)
|
||||
print(f" {len(sorted_venues) + 1}. [New venue]")
|
||||
print(f" {len(sorted_venues) + 2}. [Skip/Out of Town]")
|
||||
|
||||
choice = input("\nSelect venue number: ").strip()
|
||||
try:
|
||||
idx = int(choice) - 1
|
||||
if idx == len(sorted_venues):
|
||||
# New venue
|
||||
venue_name = input("Venue name: ").strip()
|
||||
address = input("Address: ").strip()
|
||||
beerlist = input("Beer list URL: ").strip()
|
||||
|
||||
# Add to database
|
||||
key = venue_name.lower().replace(" ", "-").replace("'", "")
|
||||
venues[key] = {
|
||||
"name": venue_name,
|
||||
"aliases": [venue_name],
|
||||
"address": address,
|
||||
"beerlist": beerlist,
|
||||
}
|
||||
save_venues(venues)
|
||||
print(f"Added {venue_name} to venue database!")
|
||||
selected_venue = venues[key]
|
||||
selected_key = key
|
||||
elif idx == len(sorted_venues) + 1:
|
||||
# Out of town / skip
|
||||
venue_name = input("Title (e.g., 'Out of Town', 'Holiday'): ").strip() or "Out of Town"
|
||||
notes = input("Notes: ").strip()
|
||||
|
||||
filepath = get_or_create_year_file(target_date.year)
|
||||
add_entry(filepath, venue_name, "NA", "NA", target_date, "DNR", notes)
|
||||
print(f"\nEntry added to {filepath.relative_to(PROJECT_ROOT)}")
|
||||
sys.exit(0)
|
||||
elif 0 <= idx < len(sorted_venues):
|
||||
selected_key, selected_venue = sorted_venues[idx]
|
||||
else:
|
||||
print("Invalid selection")
|
||||
sys.exit(1)
|
||||
except ValueError:
|
||||
print("Invalid selection")
|
||||
sys.exit(1)
|
||||
|
||||
# Collect attendees and notes
|
||||
print(f"\nVenue: {selected_venue['name']}")
|
||||
print(f"Address: {selected_venue['address']}")
|
||||
print(f"Beerlist: {selected_venue['beerlist']}")
|
||||
|
||||
attendees = input("\nAttendees (comma-separated, or 'DNR'): ").strip() or "DNR"
|
||||
notes = input("Notes: ").strip()
|
||||
|
||||
# Add to year file
|
||||
filepath = get_or_create_year_file(target_date.year)
|
||||
add_entry(
|
||||
filepath,
|
||||
selected_venue["name"],
|
||||
selected_venue["address"],
|
||||
selected_venue["beerlist"],
|
||||
target_date,
|
||||
attendees,
|
||||
notes,
|
||||
)
|
||||
|
||||
print(f"\nEntry added to {filepath.relative_to(PROJECT_ROOT)}")
|
||||
print("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create a new standalone Luna Juice post (festivals, events, special occasions).
|
||||
|
||||
Usage:
|
||||
python scripts/new_lunajuice.py # Interactive
|
||||
python scripts/new_lunajuice.py "Beer Festival 2025" # With title
|
||||
|
||||
For regular weekly beer calls, use new_beercall.py instead.
|
||||
This script is for special events that deserve their own post.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Paths
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||
CONTENT_DIR = PROJECT_ROOT / "content" / "posts" / "beercall"
|
||||
|
||||
|
||||
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 create_post(title, summary=""):
|
||||
"""Create a new Luna Juice post."""
|
||||
CONTENT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
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
|
||||
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
content = f"""+++
|
||||
title = '{title}'
|
||||
date = {now}
|
||||
draft = true
|
||||
summary = '{summary}'
|
||||
series = "Luna Juice"
|
||||
+++
|
||||
|
||||
Write about your beer adventure here...
|
||||
|
||||
"""
|
||||
|
||||
filepath.write_text(content)
|
||||
return filepath
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Create a new Luna Juice event post")
|
||||
parser.add_argument("title", nargs="?", help="Post title")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.title:
|
||||
title = args.title
|
||||
else:
|
||||
title = input("Event title: ").strip()
|
||||
if not title:
|
||||
print("Title is required")
|
||||
sys.exit(1)
|
||||
|
||||
summary = input("Summary (one line): ").strip()
|
||||
|
||||
filepath = create_post(title, summary)
|
||||
|
||||
if filepath:
|
||||
print(f"\nCreated: {filepath.relative_to(PROJECT_ROOT)}")
|
||||
print("Edit the file to add your content!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+215
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create a new Fun Center (technology) blog post.
|
||||
|
||||
Usage:
|
||||
python scripts/new_techpost.py
|
||||
python scripts/new_techpost.py "My Post Title"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Paths
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||
CONTENT_DIR = PROJECT_ROOT / "content" / "posts"
|
||||
|
||||
# Common tags for tech posts
|
||||
COMMON_TAGS = [
|
||||
"linux",
|
||||
"ubuntu",
|
||||
"self-hosted",
|
||||
"open-source",
|
||||
"privacy",
|
||||
"automation",
|
||||
"devops",
|
||||
"python",
|
||||
"homelab",
|
||||
"sdf",
|
||||
"hugo",
|
||||
]
|
||||
|
||||
|
||||
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 prompt_post_type():
|
||||
"""Ask user what type of post this is."""
|
||||
print("\nWhat kind of post is this?")
|
||||
print(" 1. How I Did It - Problem/solution, troubleshooting, tutorials")
|
||||
print(" 2. Grinds My Gears - Opinion, rant, commentary")
|
||||
print(" 3. Quick Tip - Short discovery, TIL, neat trick")
|
||||
|
||||
while True:
|
||||
choice = input("Enter 1, 2, or 3: ").strip()
|
||||
if choice == "1":
|
||||
return "how-to"
|
||||
elif choice == "2":
|
||||
return "opinion"
|
||||
elif choice == "3":
|
||||
return "quick-tip"
|
||||
else:
|
||||
print("Please enter 1, 2, or 3")
|
||||
|
||||
|
||||
def prompt_tags(post_type):
|
||||
"""Suggest and collect tags."""
|
||||
# Suggest tags based on post type
|
||||
type_tags = {
|
||||
"how-to": ["how-i-did-it", "technology"],
|
||||
"opinion": ["opinion", "technology"],
|
||||
"quick-tip": ["til", "technology"],
|
||||
}
|
||||
|
||||
suggested = type_tags.get(post_type, ["technology"])
|
||||
|
||||
print(f"\nSuggested tags: {', '.join(suggested)}")
|
||||
print(f"Common tags: {', '.join(COMMON_TAGS)}")
|
||||
|
||||
additional = input("Additional tags (comma-separated, or Enter to skip): ").strip()
|
||||
|
||||
tags = suggested.copy()
|
||||
if additional:
|
||||
for tag in additional.split(","):
|
||||
tag = tag.strip().lower().replace(" ", "-")
|
||||
if tag and tag not in tags:
|
||||
tags.append(tag)
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
def get_skeleton(post_type):
|
||||
"""Get the content skeleton based on post type."""
|
||||
if post_type == "how-to":
|
||||
return """
|
||||
## The Problem
|
||||
|
||||
What broke? What were you trying to do?
|
||||
|
||||
|
||||
## What I Tried
|
||||
|
||||
The journey - commands, dead ends, frustration...
|
||||
|
||||
|
||||
## What Worked
|
||||
|
||||
The fix! Include the commands/steps.
|
||||
|
||||
|
||||
## Hindsight
|
||||
|
||||
What would you do differently? Any gotchas for future-you?
|
||||
|
||||
"""
|
||||
elif post_type == "opinion":
|
||||
return """
|
||||
## The Thing
|
||||
|
||||
What's on your mind?
|
||||
|
||||
|
||||
## Why It Matters
|
||||
|
||||
Some context...
|
||||
|
||||
|
||||
## My Take
|
||||
|
||||
Your opinion here...
|
||||
|
||||
|
||||
## Links
|
||||
|
||||
- [Reference 1](https://example.com)
|
||||
|
||||
"""
|
||||
else: # quick-tip
|
||||
return """
|
||||
Quick tip or discovery here. Keep it short!
|
||||
|
||||
```bash
|
||||
# command or code if relevant
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def create_post(title, post_type, tags, summary):
|
||||
"""Create the post file."""
|
||||
CONTENT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
slug = slugify(title)
|
||||
filename = f"{slug}.md"
|
||||
filepath = CONTENT_DIR / filename
|
||||
|
||||
if filepath.exists():
|
||||
print(f"\nPost already exists: {filepath.relative_to(PROJECT_ROOT)}")
|
||||
overwrite = input("Overwrite? (y/N): ").strip().lower()
|
||||
if overwrite != "y":
|
||||
return None
|
||||
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
tags_yaml = "\n".join(f" - {tag}" for tag in tags)
|
||||
skeleton = get_skeleton(post_type)
|
||||
|
||||
content = f'''---
|
||||
title: '{title}'
|
||||
date: {now}
|
||||
draft: true
|
||||
series: "Fun Center"
|
||||
summary: "{summary}"
|
||||
tags:
|
||||
{tags_yaml}
|
||||
---
|
||||
{skeleton}'''
|
||||
|
||||
filepath.write_text(content)
|
||||
return filepath
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Create a new Fun Center tech post")
|
||||
parser.add_argument("title", nargs="?", help="Post title")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get title
|
||||
if args.title:
|
||||
title = args.title
|
||||
else:
|
||||
title = input("Post title: ").strip()
|
||||
if not title:
|
||||
print("Title is required")
|
||||
sys.exit(1)
|
||||
|
||||
# Get post type
|
||||
post_type = prompt_post_type()
|
||||
|
||||
# Get tags
|
||||
tags = prompt_tags(post_type)
|
||||
|
||||
# Get summary
|
||||
summary = input("\nOne-line summary: ").strip()
|
||||
|
||||
# Create the post
|
||||
filepath = create_post(title, post_type, tags, summary)
|
||||
|
||||
if filepath:
|
||||
print(f"\nCreated: {filepath.relative_to(PROJECT_ROOT)}")
|
||||
print(f"Type: {post_type}")
|
||||
print(f"Tags: {', '.join(tags)}")
|
||||
print("\nEdit the file to fill in the skeleton!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Setup script for marcus-web blog scripts
|
||||
# Run this once after cloning the repo on a new machine
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
echo "Setting up marcus-web scripts..."
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
|
||||
# Check for Python 3
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "ERROR: python3 not found. Please install Python 3."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create virtual environment if it doesn't exist
|
||||
VENV_DIR="$PROJECT_ROOT/.venv"
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
# Activate and install dependencies
|
||||
echo "Installing dependencies..."
|
||||
source "$VENV_DIR/bin/activate"
|
||||
pip install --quiet --upgrade pip
|
||||
pip install --quiet -r "$PROJECT_ROOT/requirements.txt"
|
||||
|
||||
echo ""
|
||||
echo "Done! To use the scripts, either:"
|
||||
echo ""
|
||||
echo " 1. Activate the venv first:"
|
||||
echo " source .venv/bin/activate"
|
||||
echo " python scripts/import_letterboxd.py"
|
||||
echo ""
|
||||
echo " 2. Or run directly with the venv python:"
|
||||
echo " .venv/bin/python scripts/import_letterboxd.py"
|
||||
echo ""
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"draught-house": {
|
||||
"name": "Draught House",
|
||||
"aliases": ["DH", "Draught House"],
|
||||
"address": "4112 Medical Pkwy, Austin, TX 78756",
|
||||
"beerlist": "https://www.draughthouse.com/drinks"
|
||||
},
|
||||
"abgb": {
|
||||
"name": "The ABGB",
|
||||
"aliases": ["ABGB", "The ABGB"],
|
||||
"address": "1305 W Oltorf St, Austin, TX 78704",
|
||||
"beerlist": "https://theabgb.com/beers/"
|
||||
},
|
||||
"lazarus": {
|
||||
"name": "Lazarus Brewing",
|
||||
"aliases": ["Lazarus", "Lazarus 2"],
|
||||
"address": "4803 Airport Blvd, Austin, TX 78751",
|
||||
"beerlist": "https://lazarusbrewing.com/our-beer/"
|
||||
},
|
||||
"abw": {
|
||||
"name": "Austin Beer Works",
|
||||
"aliases": ["ABW", "Austin Beer Works", "Austin Beerworks"],
|
||||
"address": "3001 Industrial Terrace, Austin, TX 78758",
|
||||
"beerlist": "https://austinbeerworks.com/tap-room"
|
||||
},
|
||||
"abw-sprinkle": {
|
||||
"name": "Austin Beer Works (Sprinkle Valley)",
|
||||
"aliases": ["Sprinkle Valley", "ABW Sprinkle"],
|
||||
"address": "10300 Springdale Rd, Austin, TX 78754",
|
||||
"beerlist": "https://austinbeerworks.com/page/welcome-to-sprinkle-valley"
|
||||
},
|
||||
"pint-house-burnet": {
|
||||
"name": "Pint House Burnet",
|
||||
"aliases": ["PHP Burnet", "Pint House Pizza Burnet", "Pinthouse Burnet"],
|
||||
"address": "4729 Burnet Rd, Austin, TX 78756",
|
||||
"beerlist": "https://pinthouse.com/burnet/beer/beer-on-tap"
|
||||
},
|
||||
"pint-house-south-lamar": {
|
||||
"name": "Pint House South Lamar",
|
||||
"aliases": ["PHP South Lamar", "Pint House Pizza South Lamar", "Pinthouse South Lamar"],
|
||||
"address": "4236 S Lamar Blvd, Austin, TX 78704",
|
||||
"beerlist": "https://pinthouse.com/ben-white/beer/beer-on-tap"
|
||||
},
|
||||
"pint-house-ben-white": {
|
||||
"name": "Pint House Ben White",
|
||||
"aliases": ["PHP Ben White", "Pint House Pizza Ben White"],
|
||||
"address": "2201 E Ben White Blvd, Austin, TX 78744",
|
||||
"beerlist": "https://pinthouse.com/ben-white/beer/beer-on-tap"
|
||||
},
|
||||
"burnet-go-to": {
|
||||
"name": "Burnet Go-To",
|
||||
"aliases": ["Burnet Go To", "Go-To", "Go To"],
|
||||
"address": "6800 Burnet Rd #2, Austin, TX 78757",
|
||||
"beerlist": "https://burnetgoto.com/draught-beer.html"
|
||||
},
|
||||
"black-star": {
|
||||
"name": "Black Star Coop",
|
||||
"aliases": ["Black Star", "Blackstar"],
|
||||
"address": "7020 Easy Wind Dr, Austin, TX 78752",
|
||||
"beerlist": "https://www.blackstar.coop/beer"
|
||||
},
|
||||
"celis": {
|
||||
"name": "Celis Brewery",
|
||||
"aliases": ["Celis"],
|
||||
"address": "10001 Metric Blvd, Austin, TX 78758",
|
||||
"beerlist": "https://www.celisbeers.com/copy-of-core-beers-1"
|
||||
},
|
||||
"batch": {
|
||||
"name": "Batch Kolache",
|
||||
"aliases": ["Batch", "Batch Kolache and Brewing"],
|
||||
"address": "3220 Manor Rd, Austin, TX 78723",
|
||||
"beerlist": "https://batchatx.com/beer/"
|
||||
},
|
||||
"easy-tiger-linc": {
|
||||
"name": "Easy Tiger Linc",
|
||||
"aliases": ["Easy Tiger", "Easy Tiger LINC"],
|
||||
"address": "6406 N Interstate Hwy 35 Ste 1100, Austin, TX 78752",
|
||||
"beerlist": "https://www.easytigerusa.com/location/easy-tiger-linc/"
|
||||
},
|
||||
"live-oak": {
|
||||
"name": "Live Oak Brewing Company",
|
||||
"aliases": ["Live Oak", "Live Oak Brewing"],
|
||||
"address": "1615 Crozier Ln, Del Valle, TX 78617",
|
||||
"beerlist": "https://liveoakbrewing.com/beer/"
|
||||
},
|
||||
"front-page": {
|
||||
"name": "Front Page",
|
||||
"aliases": ["Front Page Austin"],
|
||||
"address": "1023 Springdale Rd #1b, Austin, TX 78721",
|
||||
"beerlist": "https://www.frontpageaustin.com/beers"
|
||||
},
|
||||
"brewtorium": {
|
||||
"name": "Brewtorium",
|
||||
"aliases": ["The Brewtorium"],
|
||||
"address": "6015 Dillard Cir A, Austin, TX 78752",
|
||||
"beerlist": "https://www.thebrewtorium.com/beer"
|
||||
},
|
||||
"meanwhile": {
|
||||
"name": "Meanwhile Brewing",
|
||||
"aliases": ["Meanwhile"],
|
||||
"address": "3901 Promontory Point Dr, Austin, TX 78744",
|
||||
"beerlist": "https://www.meanwhilebeer.com/"
|
||||
},
|
||||
"oskar-blues": {
|
||||
"name": "Oskar Blues",
|
||||
"aliases": ["Oskar Blues Austin"],
|
||||
"address": "10420 Metric BLVD #150, Austin, TX 78758",
|
||||
"beerlist": "https://oskarblues.com/location/austin/#brewery-menu"
|
||||
},
|
||||
"austin-craft": {
|
||||
"name": "Austin Craft Brewing",
|
||||
"aliases": ["Austin Craft Brew"],
|
||||
"address": "4700 Burleson, Austin, TX",
|
||||
"beerlist": "https://www.austincraftbrew.com/"
|
||||
},
|
||||
"poodies": {
|
||||
"name": "Poodie's",
|
||||
"aliases": ["Poodies", "Poodie's Hilltop"],
|
||||
"address": "22308 TX-71, Spicewood, TX 78669",
|
||||
"beerlist": ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user