#!/usr/bin/env python3
"""Extract Back to Godhead magazine (1966-1973) from btg-1978.html.
Each magazine issue becomes its own JSON file.
Issues are grouped by volume/era in the intro.json metadata.
Advertisement sections are excluded.
Structure:
- p2 = Issue bookmark (bold, e.g., "Back to Godhead Magazine. Vol. 1, No. 01, October 23, 1966")
- p8 = Article bookmark within an issue
- p9 = Article display title (bold)
- p7, p5, p12, p4, p3, etc. = Body text
- Articles titled "Advertisement" are skipped entirely
Output:
lib/jsons/magazines/btg-1978/intro.json
lib/jsons/magazines/btg-1978/issues/no-01.json
lib/jsons/magazines/btg-1978/issues/no-02.json
...
lib/jsons/magazines/btg-1978/issues/no-54.json
Usage:
python3 scripts/btg_1978_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 → text (italic marker in btg-1978)
html = re.sub(
r']*class="s2"[^>]*>(.*?)', r"\1", html, flags=re.DOTALL
)
# Remove all other span tags but keep inner content
html = re.sub(r"]*>(.*?)", r"\1", html, flags=re.DOTALL)
# Remove Apple-tab-span and tab characters
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 write_json(data: dict, path: str):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
# ---------------------------------------------------------------------------
# Parse issue info from p2 text
# ---------------------------------------------------------------------------
def parse_issue_info(text: str):
"""Parse issue info from the p2 bookmark text.
Handles various formats:
"Back to Godhead Magazine. Vol. 1, No. 01, October 23, 1966"
"Back to Godhead Magazine. Vol. 1, No. 06, January 20, 1967"
"Back to Godhead Magazine. [VOL II No 1.] No. 16, 1968"
"Back to Godhead Magazine VOL II. No 2. [#17], 1968"
"Back to Godhead Magazine no. 30, 1969"
"Back to Godhead Magazine No. 36, 1970"
Returns (issue_number, volume, display_title, date_str, slug) or None.
"""
text = text.strip()
# Remove "Back to Godhead Magazine" prefix variations
text_clean = re.sub(
r"^Back to Godhead Magazine\.?\s*", "", text, flags=re.IGNORECASE
).strip()
issue_num = None
volume = None
date_str = ""
# Try to extract issue number from various patterns
# Pattern 1: "Vol. X, No. YY, date" or "Vol. X No. YY, date"
m = re.search(
r"Vol\.?\s*(\d+)\.?\s*,?\s*No\.?\s*(\d+)\s*,?\s*(.*)", text_clean, re.IGNORECASE
)
if m:
volume = int(m.group(1))
issue_num = int(m.group(2))
date_str = m.group(3).strip().rstrip(",. ")
# Pattern 2: "[VOL II No 1.] No. 16, 1968" — use the absolute number
if issue_num is None:
m = re.search(r"No\.?\s*(\d+)\s*,?\s*(\d{4})", text_clean, re.IGNORECASE)
if m:
issue_num = int(m.group(1))
date_str = m.group(2).strip()
# Pattern 3: "VOL II. No 2. [#17], 1968"
if issue_num is None:
m = re.search(r"#(\d+)", text_clean)
if m:
issue_num = int(m.group(1))
# Pattern 4: "no. 30, 1969" or "no 31, 1969"
if issue_num is None:
m = re.search(r"no\.?\s*(\d+)\s*,?\s*(.*)", text_clean, re.IGNORECASE)
if m:
issue_num = int(m.group(1))
date_str = m.group(2).strip().rstrip(",. ")
# Try to extract volume from text if not found yet
if volume is None:
m = re.search(r"VOL\.?\s*(?:II|2)", text_clean, re.IGNORECASE)
if m:
volume = 2
else:
m = re.search(r"VOL\.?\s*(?:I|1)", text_clean, re.IGNORECASE)
if m:
volume = 1
if issue_num is None:
print(f" ⚠ Could not parse issue number from: {text}")
return None
# Extract year from date_str if present
year_match = re.search(r"(\d{4})", date_str)
year = int(year_match.group(1)) if year_match else None
# Build display title
if volume and issue_num <= 17:
display_title = f"Vol. {volume}, No. {issue_num:02d}"
if date_str:
display_title += f", {date_str}"
else:
display_title = f"No. {issue_num}"
if date_str:
display_title += f", {date_str}"
slug = f"no-{issue_num:02d}"
return issue_num, volume, display_title, date_str, year, slug
# ---------------------------------------------------------------------------
# Main extraction
# ---------------------------------------------------------------------------
def main():
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
html_path = os.path.join(base_dir, "public", "html", "btg-1978.html")
output_dir = os.path.join(base_dir, "lib", "jsons", "magazines", "btg-1978")
issues_dir = os.path.join(output_dir, "issues")
if not os.path.exists(html_path):
print(f"ERROR: {html_path} not found!")
sys.exit(1)
print(f"Reading {html_path}...")
with open(html_path, "r", encoding="utf-8") as f:
content = f.read()
soup = BeautifulSoup(content, "html.parser")
all_tags = soup.find_all("p")
print(f" Parsed {len(all_tags)}
tags total")
# ---------------------------------------------------------------------------
# Phase 1: Find all issue boundaries
# ---------------------------------------------------------------------------
issue_boundaries = [] # List of (tag_index, issue_num, volume, display_title, date_str, year, slug)
for i, tag in enumerate(all_tags):
cls = get_css_class(tag)
if cls == "p2":
text = tag.get_text().strip()
if "Back to Godhead" in text:
parsed = parse_issue_info(text)
if parsed:
issue_num, volume, display_title, date_str, year, slug = parsed
issue_boundaries.append(
(i, issue_num, volume, display_title, date_str, year, slug)
)
print(f"\n Found {len(issue_boundaries)} issues")
for ib in issue_boundaries:
tag_idx, inum, vol, title, ds, yr, slug = ib
print(f" tag[{tag_idx}]: #{inum} (Vol {vol}) {title} → {slug}")
# ---------------------------------------------------------------------------
# Phase 2: Extract articles and paragraphs for each issue
# ---------------------------------------------------------------------------
# Known skip classes (bookmarks, empty lines)
SKIP_CLASSES = {"p2", "p11", "p28", "p24"}
issues_data = []
skipped_ads = 0
# Maximum paragraphs per issue — prevents mega-issues from absorbing
# unmarked content that belongs to missing intermediate issues.
MAX_PARAGRAPHS_PER_ISSUE = 500
for idx, (
start_tag_idx,
issue_num,
vol,
display_title,
date_str,
year,
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 = []
in_advertisement = False
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:
continue
# p8 = Article bookmark
if cls == "p8":
article_title = decode_scagoudy(text).strip()
# Check if this is an advertisement
if re.match(r"^Advertisements?", article_title, re.IGNORECASE):
in_advertisement = True
skipped_ads += 1
continue
else:
in_advertisement = False
articles.append({"title": article_title})
continue
# p122 and other classes can also be advertisement markers
if re.match(r"^Advertisements?\s*$", text, re.IGNORECASE):
in_advertisement = True
skipped_ads += 1
continue
# p9 = Article display title
if cls == "p9":
if in_advertisement:
continue
# Also check the p9 display title itself for advertisement
plain_title = decode_scagoudy(text).strip()
if re.match(r"^Advertisements?\s*$", plain_title, re.IGNORECASE):
in_advertisement = True
skipped_ads += 1
continue
html = inner_html(tag)
if html:
# Make it a bold article heading if not already
if not html.startswith(""):
html = f"{html}"
paragraphs.append(html)
continue
# Skip content within advertisement sections
if in_advertisement:
# Check if next p8 will reset it — for now just skip
continue
# p15 = Verse block — center align
if cls == "p15":
html = inner_html(tag)
if html:
paragraphs.append(f'
":
# Check if this looks like a verse (entirely italic with
breaks)
stripped = html.strip()
if (
stripped.startswith("")
and "
' not in stripped
):
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'