Skip to content

Instantly share code, notes, and snippets.

@jooray
Created December 5, 2024 22:51
Converts a LearnDash course backup (lessons only) into a single ordered HTML file with sections for easy reference.
#!/usr/bin/python3
# This script converts a learndash course backup with lessons (but not
# themes/topics!) to a single ordered HTML file with sections for easy reference
import argparse
import json
def analyze_course_structure(course_file, lessons_file, course_id):
with open(course_file, 'r', encoding='utf-8') as file:
lines = file.readlines()
course_title = "Untitled Course"
course_sections = []
lessons = {}
lesson_ids_in_order = []
# Parse course file for the given course_id
for line in lines:
try:
obj = json.loads(line)
if obj.get("wp_post", {}).get("ID") == course_id:
wp_post_meta = obj.get("wp_post_meta", {})
course_title = obj.get("wp_post", {}).get("post_title", "Untitled Course")
course_sections_raw = wp_post_meta.get("course_sections", [])
# Decode course_sections
decoded_sections = []
if (isinstance(course_sections_raw, list) and len(course_sections_raw) == 1 and isinstance(course_sections_raw[0], str)):
try:
decoded_sections = json.loads(course_sections_raw[0])
except json.JSONDecodeError:
decoded_sections = []
elif isinstance(course_sections_raw, str):
try:
decoded_sections = json.loads(course_sections_raw)
except json.JSONDecodeError:
decoded_sections = []
elif isinstance(course_sections_raw, list):
decoded_sections = course_sections_raw
# Extract section headings
for section in decoded_sections:
if isinstance(section, dict) and section.get("type") == "section-heading":
s_order = section.get("order", 0)
s_title = section.get("post_title", "Untitled Section")
course_sections.append({
"order": int(s_order),
"title": s_title
})
# Extract lesson order from ld_course_steps
steps = wp_post_meta.get("ld_course_steps", [])
if steps and len(steps) > 0:
sfwd_lessons = steps[0].get("steps", {}).get("h", {}).get("sfwd-lessons", {})
lesson_ids_in_order = list(sfwd_lessons.keys())
break
except json.JSONDecodeError:
pass
# Parse lessons
with open(lessons_file, 'r', encoding='utf-8') as f:
for line in f:
try:
obj = json.loads(line)
wp_post = obj.get("wp_post", {})
lesson_id = str(wp_post.get("ID"))
if lesson_id in lesson_ids_in_order:
lessons[lesson_id] = {
"title": wp_post.get("post_title", "Untitled Lesson"),
"content": wp_post.get("post_content", "")
}
except json.JSONDecodeError:
pass
# Start with all lessons in order
combined_structure = [lessons[lid]["title"] for lid in lesson_ids_in_order]
# Sort sections by ascending order
course_sections.sort(key=lambda x: x["order"])
# Insert each section at its final desired position
for sec in course_sections:
insert_index = sec["order"]
if insert_index > len(combined_structure):
insert_index = len(combined_structure)
combined_structure.insert(insert_index, "!" + sec["title"])
return course_title, combined_structure, lessons
def generate_html(course_title, structure, lessons):
html_output = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{course_title}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<style>
.wp-block-embed {{ display: none; }}
</style>
</head>
<body class="bg-light" id="top">
<div class="container py-5">
<h1 class="text-center mb-4">{course_title}</h1>
<nav>
<h2>Table of Contents</h2>
<ul class="list-group mb-4">
"""
for line in structure:
if line.startswith("!"):
html_output += f'<li class="list-group-item bg-secondary text-white"><strong>{line[1:]}</strong></li>'
else:
# Find the corresponding lesson ID
lesson_id = next((lid for lid, l in lessons.items() if l["title"] == line), None)
if lesson_id:
html_output += f'<li class="list-group-item ms-3"><a href="#lesson-{lesson_id}">{line}</a></li>'
html_output += """
</ul>
</nav>
"""
for line in structure:
if line.startswith("!"):
# Section heading
html_output += f'<h1 class="my-4 text-primary">{line[1:]}</h1>'
else:
# Lesson
lesson_id = next((lid for lid, l in lessons.items() if l["title"] == line), None)
lesson = lessons.get(lesson_id, {"title": "Unknown Lesson", "content": ""})
html_output += f'<h2 id="lesson-{lesson_id}" class="mt-3">{lesson["title"]}</h2>\n'
html_output += f'<p><a href="#top">Back to Table of Contents</a></p>\n'
html_output += f'<div>{lesson["content"]}</div>\n'
html_output += """
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
"""
return html_output
def main():
parser = argparse.ArgumentParser(description="Generate course HTML from LearnDash export files.")
parser.add_argument("course_id", type=int, help="ID of the course to process")
parser.add_argument("course_file", type=str, help="Path to the course file")
parser.add_argument("lessons_file", type=str, help="Path to the lessons file")
parser.add_argument("output_file", type=str, help="Path to the output HTML file")
args = parser.parse_args()
course_title, structure, lessons = analyze_course_structure(args.course_file, args.lessons_file, args.course_id)
html_output = generate_html(course_title, structure, lessons)
with open(args.output_file, "w", encoding="utf-8") as f:
f.write(html_output)
print(f"HTML output saved to {args.output_file}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment