#!/usr/bin/env python3
"""Extract Back to Godhead magazine from btg-1960.html.
Each magazine issue becomes its own JSON file.
Issues are grouped by volume in the intro.json metadata.
Structure:
- p2 = Volume/Part bookmark (e.g., "BTGPY1: Volume 1, Part 1, 1944")
- p3 = Volume/Part display title
- p8 = Article bookmark within an issue (e.g., "BTGPY1a: Message of His Divine Grace")
- p9 = Article display title (bold)
- p12, p35 = Body paragraphs
- p14, p17, p88 = Continuation notes / misc text
- p15 = Verse blocks (italic, with ScaGoudy encoding)
- p33, p40, p47, p54 = Magazine mastheads/headers
Output:
lib/jsons/magazines/btg/intro.json
lib/jsons/magazines/btg/issues/v1-p1.json
lib/jsons/magazines/btg/issues/v1-p2.json
...
lib/jsons/magazines/btg/issues/v4-p1.json
Usage:
python3 scripts/btg_extract.py
"""
import json
import os
import re
import sys
from bs4 import BeautifulSoup, Tag
# ---------------------------------------------------------------------------
# ScaGoudy → IAST Unicode mapping
# ---------------------------------------------------------------------------
SCAGOUDY_TO_IAST = {
"ä": "ā",
"Ä": "Ā",
"å": "ṛ",
"Å": "Ṛ",
"ñ": "ṣ",
"Ñ": "Ṣ",
"ö": "ṭ",
"Ö": "Ṭ",
"ë": "ṇ",
"Ë": "Ṇ",
"ò": "ḍ",
"Ò": "Ḍ",
"ç": "ś",
"Ç": "Ś",
"ì": "ṅ",
"Ì": "Ṅ",
"ï": "ñ",
"Ï": "Ñ",
"ü": "ū",
"Ü": "Ū",
"é": "ī",
"É": "Ī",
"à": "ṁ",
"ù": "ḥ",
"è": "ṝ",
"ÿ": "ḷ",
}
def decode_scagoudy(text: str) -> str:
"""Replace ScaGoudy-encoded characters with proper IAST Unicode."""
for src, dst in SCAGOUDY_TO_IAST.items():
text = text.replace(src, dst)
return text
def get_css_class(tag: Tag) -> str:
classes = tag.get("class", [])
return classes[0] if classes else ""
def inner_html(tag: Tag) -> str:
"""Get inner HTML of a tag, decode ScaGoudy, clean up spans."""
html = "".join(str(child) for child in tag.children)
html = decode_scagoudy(html)
# text → keep inner text (s1 is a ref style)
html = re.sub(
r']*class="s1"[^>]*>(.*?)', r"\1", html, flags=re.DOTALL
)
# text → keep inner text (superscript digit)
html = re.sub(
r']*class="s3"[^>]*>(.*?)', r"\1", html, flags=re.DOTALL
)
# text → keep inner text
html = re.sub(
r']*class="s4"[^>]*>(.*?)', r"\1", html, flags=re.DOTALL
)
# text → keep inner text
html = re.sub(
r']*class="s5"[^>]*>(.*?)', r"\1", html, flags=re.DOTALL
)
# text → keep inner text
html = re.sub(
r']*class="s7"[^>]*>(.*?)', r"\1", html, flags=re.DOTALL
)
# text → keep inner text
html = re.sub(
r']*class="s8"[^>]*>(.*?)', r"\1", html, flags=re.DOTALL
)
# Remove any remaining spans but keep content
html = re.sub(r"]*>(.*?)", r"\1", html, flags=re.DOTALL)
# Remove Apple-tab-span
html = html.replace("\t", " ")
# Collapse multiple spaces (but not around
)
html = re.sub(r"[ ]+", " ", html)
html = html.strip()
# Remove leading
tags
html = re.sub(r"^(
[\s]*)+", "", html)
# Remove trailing
tags
html = re.sub(r"(
[\s]*)+$", "", html)
return html
def is_verse_paragraph(tag: Tag) -> bool:
"""Check if a paragraph is a verse block (italic with line breaks)."""
cls = get_css_class(tag)
# p15 is commonly used for verse blocks
if cls == "p15":
return True
# Also check for paragraphs that are entirely italic with
breaks
# (some verses use p14 or other classes but are entirely in tags)
return False
def format_verse(tag: Tag) -> str:
"""Format a verse paragraph as a center-aligned div."""
html = inner_html(tag)
if not html:
return ""
# Wrap in center-aligned div
return f'
tags total")
# ---------------------------------------------------------------------------
# Phase 1: Find all issue boundaries
# ---------------------------------------------------------------------------
# Each issue starts with a p2 bookmark tag
issue_boundaries = [] # List of (tag_index, volume, part, date_str, slug)
for i, tag in enumerate(all_tags):
cls = get_css_class(tag)
if cls == "p2":
text = tag.get_text().strip()
parsed = parse_volume_part(text)
if parsed:
vol, part, date_str, slug = parsed
issue_boundaries.append((i, vol, part, date_str, slug))
print(f"\n Found {len(issue_boundaries)} issues")
for ib in issue_boundaries:
print(f" tag[{ib[0]}]: Vol {ib[1]}, Part {ib[2]}, {ib[3]} → {ib[4]}")
# ---------------------------------------------------------------------------
# Phase 2: Extract articles and paragraphs for each issue
# ---------------------------------------------------------------------------
# Classes to skip entirely
SKIP_CLASSES = {"p2", "p11", "p24", "p28"}
# Classes that are header/masthead material (include as paragraphs)
MASTHEAD_CLASSES = {"p3", "p4", "p5", "p6", "p7", "p32", "p33", "p34", "p40", "p47"}
# Classes that are body text
BODY_CLASSES = {
"p10",
"p12",
"p13",
"p14",
"p15",
"p16",
"p17",
"p35",
"p36",
"p37",
"p38",
"p39",
"p41",
"p42",
"p43",
"p44",
"p45",
"p46",
"p48",
"p49",
"p50",
"p51",
"p52",
"p53",
"p54",
"p55",
"p56",
"p57",
"p58",
"p59",
"p60",
"p61",
"p62",
"p63",
"p64",
"p65",
"p66",
"p67",
"p68",
"p69",
"p70",
"p71",
"p72",
"p73",
"p74",
"p75",
"p76",
"p77",
"p78",
"p79",
"p80",
"p81",
"p82",
"p83",
"p84",
"p85",
"p86",
"p87",
"p88",
"p89",
"p90",
"p91",
"p92",
"p93",
"p94",
"p95",
"p96",
"p97",
"p98",
"p99",
}
issues_data = []
for idx, (start_tag_idx, vol, part, date_str, slug) in enumerate(issue_boundaries):
# Determine end boundary
if idx + 1 < len(issue_boundaries):
end_tag_idx = issue_boundaries[idx + 1][0]
else:
end_tag_idx = len(all_tags)
issue_tags = all_tags[start_tag_idx:end_tag_idx]
paragraphs = []
articles = [] # List of { title, slug }
for tag in issue_tags:
cls = get_css_class(tag)
text = tag.get_text().strip()
# Skip bookmark tags and empty lines
if cls in SKIP_CLASSES:
continue
if not text and cls not in ("p15",):
continue
# p8 = Article bookmark — extract title for metadata
if cls == "p8":
# Extract article title from bookmark text (remove BTGPYxx: prefix)
article_text = text
m = re.match(r"BTGPY\d+[a-z]?:\s*(.*)", article_text)
if m:
article_title = m.group(1).strip()
else:
article_title = article_text
article_title = decode_scagoudy(article_title)
articles.append({"title": article_title})
continue
# p9 = Article display title — render as bold heading
if cls == "p9":
html = inner_html(tag)
if html:
# Make it a bold article heading
if not html.startswith(""):
html = f"{html}"
paragraphs.append(html)
continue
# p15 = Verse block — center align
if cls == "p15":
html = format_verse(tag)
if html:
paragraphs.append(html)
continue
# p3 = Volume title — skip (it's the display title of the issue header)
if cls == "p3":
continue
# Masthead classes — include but as regular paragraphs
if cls in MASTHEAD_CLASSES:
html = inner_html(tag)
if html and html != "
":
paragraphs.append(html)
continue
# Body text classes
html = inner_html(tag)
if html and html != "
":
# Check if this looks like a verse (entirely italic with
breaks)
stripped = html.strip()
if (
stripped.startswith("")
and "
' in stripped
):
# Check if it's predominantly a verse (italic block with line breaks)
# by seeing if removing the tags leaves mostly text with
plain = re.sub(r"?[bi]>", "", stripped)
lines = [
l.strip() for l in re.split(r"
", plain) if l.strip()
]
if len(lines) >= 2:
html = f'