Files
documentamusica/generate_catalog.py
2025-10-03 11:09:06 +02:00

719 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Generate static HTML catalog pages from SQL dump
"""
import re
import json
from pathlib import Path
from html import escape
# Language mappings
LANG_NAMES = {
'DE': 'Deutsch',
'EN': 'English',
'FR': 'Français'
}
# Composer names for file generation
COMPOSERS = ['Bach', 'Beethoven', 'Brahms', 'Buxtehude', 'Chopin', 'Mozart', 'Schumann', 'Wagner', 'Wieck']
# German mappings (from dataDE.inc.php)
GENRE_DE = {
"am": "Abendmusiken", "hoch": "Hochzeitsarien", "cas": "Kanons", "cantn": "Kantaten",
"lit": "Liturgische Werke", "cla": "Werke für Klavier", "worg": "Werke für Orgel",
"ws": "Werke für Streicher mit Basso continuo", "wom": "Werke ohne Musik",
"art": "Die Kunst der Fuge", "cano": "Kanon", "cant": "Kantate", "cha": "Kammermusik",
"chov": "Choral, vierstimmig", "con": "Konzert", "cpia": "Konzert für zwei bis vier Klaviere",
"csol": "Konzert für ein oder mehrere Soloinstrumente", "hymn": "Lied", "lut": "Musik für Laute",
"mass": "Messe", "mot": "Motette", "off": "Musikalisches Opfer", "ora": "Oratorium",
"org": "Musik für Orgel", "over": "Ouvertüre", "pas": "Passionsmusik",
"pian": "Musik für Klavier und Cembalo", "imos": "Instrumentalmusik für Orchester: Symphonie",
"imoo": "Instrumentalmusik für Orchester: Ouverture", "imob": "Instrumentalmusik für Orchester: Ballettmusik",
"imoa": "Instrumentalmusik für Orchester: Andere Werke", "imb": "Instrumentalmusik: für Blasinstrumente",
"imsso": "Instrumentalmusik: für mehrere Soloinstrumente und Orchester",
"imko": "Instrumentalmusik für Klavier und Orchester", "imvo": "Instrumentalmusik für Violine und Orchester",
"kammk": "Instrumentalmusik: Kammermusik mit Klavier", "kamok": "Instrumentalmusik: Kammermusik ohne Klavier",
"imkv": "Instrumentalmusik für Klavier zu vier Händen", "imkzs": "Instrumentalmusik für Klavier zu zwei Händen: Sonate",
"imkzv": "Instrumentalmusik für Klavier zu zwei Händen: Variation", "imkzt": "Instrumentalmusik für Klavier zu zwei Händen: Tanz",
"imkza": "Instrumentalmusik für Klavier zu zwei Händen: Andere Werke", "imsa": "Instrumentalmusik: Solostücke für andere Instrumente",
"vmu": "Vokalmusik", "vmumo": "Vokalmusik: Messe, Oratorium", "vmuo": "Vokalmusik: Oper, Bühnenmusik",
"vmub": "Vokalmusik: für eine oder mehrer Stimmen mit Begleitung", "vmuob": "Vokalmusik: für eine oder mehrer Stimmen ohne Begleitung",
"vmuk": "Vokalmusik: Kanon", "vmus": "Vokalmusik: Musikalischer Scherz", "bal": "Ballade",
"ein": "Einzelstück", "etu": "Etude", "fug": "Fuge, Kanon", "imp": "Impromptu", "kam": "Kammermusik",
"lie": "Lied", "maz": "Mazurka", "noc": "Nocturne", "pol": "Polonaise", "pre": "Prélude",
"ron": "Rondo", "sch": "Scherzo", "son": "Sonate", "var": "Variation", "wal": "Walzer",
"vom": "Messe", "vol": "Litanei", "vog": "geistlicher Gesang", "voo": "Kantate, Oratorium",
"vop": "Oper", "voa": "Arie, Duett, Trio, Quartett, mit oder ohne Begleitung", "vlk": "Lied mit Klavierbegleitung",
"vok": "Kanon", "iou": "Ouvertüre", "isy": "Symphonie", "ise": "Serenade, Divertimento",
"ima": "Marsch, Einzelsatz, kleineres Stück", "ita": "Tanz",
"iks": "Konzert für Saiten- oder Blasinstrumente mit Orchester", "cqs": "Streichquintett, -quartett",
"cds": "Streich-Duo, -Trio", "kor": "Musik für ein, zwei oder drei Klaviere und Orchester",
"kqt": "Trio, Quartett, Quintett für Klavier oder Cembalo", "ksv": "Sonate für Tasteninstrument und Violine",
"kss": "Sonate für Tasteninstrument und Streicher", "kvh": "Musik für Klavier oder Cembalo zu vier Händen",
"ksp": "Sonate, Phantasie für Tasteninstrument", "kva": "Variationen für Tasteninstrument",
"kks": "kleineres Stück für Tasteninstrument", "sio": "Sonate für verschiedene Instrumente und Orgel",
"sup": "unvollendetes oder zweifelhaftes Werk", "arr": "Arrangement", "cho": "Chormusik",
"exe": "Studienwerk", "kkm": "Klavier- und Kammermusik", "opu": "Oper (unvollendet)",
"opv": "Oper", "orc": "Orchesterwerk", "sce": "Schauspielmusik", "the": "Einzelthema oder Melodie",
"vor": "Arie mit Orchester", "xcp": "Kammermusik: Klarinette und Klavier",
"xqc": "Kammermusik: Klarinettenquintett", "xpo": "Kammermusik: Klavier oder Orgel",
"xpvh": "Kammermusik: Klavier zu vier Händen", "xtqqp": "Kammermusik: Klaviertrios, -quartette, -quintett",
"xst": "Kammermusik: Streichinstrumente", "xvp": "Kammermusik: Violine und Klavier",
"xvcp": "Kammermusik: Violoncello und Klavier", "xpd": "Kammermusik: zwei Klaviere",
"xos": "Orchester mit Soloinstrument", "xow": "Orchesterwerke", "xch": "Vokalmusik: Chöre",
"xdgp": "Vokalmusik: Duette mit Klavier", "xgp": "Vokalmusik: einstimmige Lieder mit Klavier",
"xgins": "Vokalmusik: Lieder und Chöre mit mehreren Instrumenten",
"xgmp": "Vokalmusik: mehrstimmige Gesänge mit Klavier oder Orgel",
"xg": "Vokalmusik: mehrstimmige Gesänge ohne Begleitung",
"ybew": "Bearbeitungen von Werken anderer Komponisten", "ybum": "Bühnenmusik",
"yccf": "Chormusik a cappella, Frauenstimen", "yccg": "Chormusik a cappella, gemischte Stimmen",
"yccm": "Chormusik a cappella, Männerstimmen", "ycco": "Chorwerke mit Orchester",
"ydtg": "Duette und Trios für Gesang", "ykfs": "Kammermusik für Streicher",
"ykmp": "Kammermusik mit Klavier", "ykmo": "Konzerte mit Orchester", "ylie": "Lieder",
"yvgp": "Mehrstimmige Gesänge mit Klavier oder Orgel", "ypvh": "Musik für Klavier zu vier Händen",
"ypzh": "Musik für Klavier zu zwei Händen", "ymzp": "Musik für zwei Klaviere",
"ywfo": "Werke für Orchester", "yorg": "Werke für Orgel",
"wschor": "Vokalmusik: Chöre", "wslied": "Lied", "wsorchkm": "Orchester/Kammermusik", "wspi": "Klavier"
}
TONART_DE = {
"cdur": "C-Dur", "fdur": "F-Dur", "bdur": "B-Dur", "esdur": "Es-Dur", "asdur": "As-Dur",
"desdur": "Des-Dur", "gesdur": "Ges-Dur", "cesdur": "Ces-Dur", "gdur": "G-Dur", "ddur": "D-Dur",
"adur": "A-Dur", "edur": "E-Dur", "hdur": "H-Dur", "fisdur": "Fis-Dur", "cisdur": "Cis-Dur",
"amoll": "a-Moll", "dmoll": "d-Moll", "gmoll": "g-Moll", "cmoll": "c-Moll", "fmoll": "f-Moll",
"bmoll": "b-Moll", "esmoll": "es-Moll", "asmoll": "as-Moll", "emoll": "e-Moll", "hmoll": "h-Moll",
"fismoll": "fis-Moll", "cismoll": "cis-Moll", "gismoll": "gis-Moll", "dismoll": "dis-Moll", "aismoll": "ais-Moll"
}
BESETZUNG_DE = {
"-al-": "Alt", "-ba-": "Bass", "-bn-": "Fagott", "-cb-": "Kontrabass", "-cem-": "Cembalo",
"-choir-": "Chor", "-cl-": "Klarinette", "-clo-": "Clarino", "-co-": "Horn", "-cont-": "Continuo",
"-fl-": "Flöte", "-gh-": "Glasharmonica", "-ha-": "Harfe", "-lu-": "Laute", "-man-": "Mandoline",
"-ob-": "Oboe", "-orch-": "Orchester", "-org-": "Orgel", "-pi-": "Klavier", "-so-": "Sopran",
"-taille-": "Taille", "-tamburi-": "Tamburi", "-tb-": "Posaune", "-te-": "Tenor", "-tm-": "Pauke/Trommel",
"-tp-": "Trompete", "-va-": "Viola", "-vadagamba-": "Viola da Gamba", "-vc-": "Violoncello",
"-vn-": "Violine", "-vo-": "Stimme", "-vs-": "Stimmen", "-vnpic-": "Violino piccolo"
}
# English mappings (abbreviated for brevity - add full mappings based on dataEN.inc.php)
GENRE_EN = {**GENRE_DE} # Simplified - should use full English translations
TONART_EN = {
"cdur": "C major", "fdur": "F major", "bdur": "B flat major", "esdur": "E flat major",
"asdur": "A flat major", "desdur": "D flat major", "gesdur": "G flat major", "cesdur": "C flat major",
"gdur": "G major", "ddur": "D major", "adur": "A major", "edur": "E major", "hdur": "B major",
"fisdur": "F sharp major", "cisdur": "C sharp major", "amoll": "A minor", "dmoll": "D minor",
"gmoll": "G minor", "cmoll": "C minor", "fmoll": "F minor", "bmoll": "B flat minor",
"esmoll": "E flat minor", "asmoll": "A flat minor", "emoll": "E minor", "hmoll": "B minor",
"fismoll": "F sharp minor", "cismoll": "C sharp minor", "gismoll": "G sharp minor",
"dismoll": "D sharp minor", "aismoll": "A sharp minor"
}
BESETZUNG_EN = {**BESETZUNG_DE} # Simplified
# French mappings (abbreviated - use full mappings)
GENRE_FR = {**GENRE_DE} # Simplified
TONART_FR = {
"cdur": "ut majeur", "fdur": "fa majeur", "bdur": "si bémol majeur", "esdur": "mi bémol majeur",
"asdur": "la bémol majeur", "desdur": "ré bémol majeur", "gesdur": "sol bémol majeur",
"cesdur": "ut bémol majeur", "gdur": "sol majeur", "ddur": "ré majeur", "adur": "la majeur",
"edur": "mi majeur", "hdur": "si majeur", "fisdur": "fa dièse majeur", "cisdur": "ut dièse majeur",
"amoll": "la mineur", "dmoll": "ré mineur", "gmoll": "sol mineur", "cmoll": "ut mineur",
"fmoll": "fa mineur", "bmoll": "si bémol mineur", "esmoll": "mi bémol mineur",
"asmoll": "la bémol mineur", "emoll": "mi mineur", "hmoll": "si mineur",
"fismoll": "fa dièse mineur", "cismoll": "ut dièse mineur", "gismoll": "sol dièse mineur",
"dismoll": "ré dièse mineur", "aismoll": "la dièse mineur"
}
BESETZUNG_FR = {**BESETZUNG_DE} # Simplified
# Mapping tables by language
GENRE_MAP = {'DE': GENRE_DE, 'EN': GENRE_EN, 'FR': GENRE_FR}
TONART_MAP = {'DE': TONART_DE, 'EN': TONART_EN, 'FR': TONART_FR}
BESETZUNG_MAP = {'DE': BESETZUNG_DE, 'EN': BESETZUNG_EN, 'FR': BESETZUNG_FR}
def parse_sql_dump(sql_file):
"""Parse SQL dump and extract work data"""
print(f"Parsing {sql_file}...")
with open(sql_file, 'r', encoding='latin-1') as f:
lines = f.readlines()
all_works = []
in_insert = False
current_insert_lines = []
for line in lines:
if line.startswith('INSERT INTO `daten` VALUES'):
in_insert = True
current_insert_lines = []
continue
elif in_insert:
if line.strip() == '' or line.startswith('/*!') or line.startswith('--'):
continue
if line.rstrip().endswith(';'):
# End of this INSERT block
current_insert_lines.append(line.rstrip(';\n'))
# Process this block
all_works.extend(parse_insert_block('\n'.join(current_insert_lines)))
in_insert = False
current_insert_lines = []
print(f" Processed INSERT block: {len(all_works)} total works so far")
else:
current_insert_lines.append(line.rstrip('\n'))
print(f"Parsed {len(all_works)} works")
return all_works
def parse_insert_block(full_text):
"""Parse a single INSERT block"""
works = []
# Split by '),\n(' to get individual records
records = re.split(r'\),\s*\n\(', full_text)
for record in records:
# Clean up the record
record = record.strip('()')
# Parse the CSV-like values (handling quoted strings with commas)
values = parse_values(record)
if len(values) >= 32: # Ensure we have all fields
work = {
'Komponist': values[0],
'TitelDE': values[1],
'TitelEN': values[2],
'TitelFR': values[3],
'TitelIT': values[4],
'TitelLA': values[5],
'TitelPL': values[6],
'IncipitDE': values[7],
'IncipitEN': values[8],
'IncipitFR': values[9],
'IncipitIT': values[10],
'IncipitLA': values[11],
'IncipitPL': values[12],
'Genre': values[13],
'Besetzung': values[14],
'Tonart': values[15],
'Jahr': values[16],
'WerkNr': values[17],
'BemerkungDE': values[18],
'BemerkungEN': values[19],
'BemerkungFR': values[20],
'sorWerkNr': values[32] if len(values) > 32 else 0
}
works.append(work)
return works
def parse_values(record):
"""Parse SQL VALUES - handle quoted strings with commas/quotes"""
values = []
current = []
in_quote = False
i = 0
while i < len(record):
char = record[i]
if char == "'" and (i == 0 or record[i-1] != '\\'):
in_quote = not in_quote
i += 1
continue
if char == ',' and not in_quote:
val = ''.join(current).strip()
# Remove quotes if present
if val.startswith("'") and val.endswith("'"):
val = val[1:-1]
values.append(val)
current = []
i += 1
# Skip whitespace after comma
while i < len(record) and record[i] in ' \t':
i += 1
continue
# Handle escaped quotes
if char == '\\' and i + 1 < len(record) and record[i+1] == "'":
current.append("'")
i += 2
continue
current.append(char)
i += 1
# Add last value
if current:
val = ''.join(current).strip()
# Remove quotes if present
if val.startswith("'") and val.endswith("'"):
val = val[1:-1]
values.append(val)
return values
def decode_genre(code, lang='DE'):
"""Decode genre code to text"""
return GENRE_MAP[lang].get(code, code)
def decode_tonart(code, lang='DE'):
"""Decode tonart (key) code to text"""
return TONART_MAP[lang].get(code, code)
def decode_besetzung(code, lang='DE'):
"""Decode besetzung (instrumentation) codes"""
if not code:
return ''
instruments = []
for key, value in BESETZUNG_MAP[lang].items():
if key in code:
instruments.append(value)
return ', '.join(instruments) if instruments else code
def generate_work_html(work, lang='DE'):
"""Generate HTML for a single work entry"""
title_field = f'Titel{lang}'
incipit_field = f'Incipit{lang}'
bemerkung_field = f'Bemerkung{lang}'
title = escape(work.get(title_field, ''))
incipit = escape(work.get(incipit_field, ''))
werknr = escape(work.get('WerkNr', ''))
jahr = escape(work.get('Jahr', ''))
genre = escape(decode_genre(work.get('Genre', ''), lang))
tonart = escape(decode_tonart(work.get('Tonart', ''), lang))
besetzung = escape(decode_besetzung(work.get('Besetzung', ''), lang))
bemerkung = escape(work.get(bemerkung_field, ''))
html = '<tr valign="top">\n'
html += f' <td width="30"></td>\n'
# Work number
if werknr:
html += f' <td width="100"><b>{werknr}</b></td>\n'
else:
html += ' <td width="100"></td>\n'
# Work details
html += ' <td>\n'
if title:
html += f' <b>{title}</b><br>\n'
if incipit:
html += f' <i>{incipit}</i><br>\n'
details = []
if genre:
details.append(f'Genre: {genre}')
if tonart:
details.append(f'Tonart: {tonart}' if lang == 'DE' else f'Key: {tonart}' if lang == 'EN' else f'Tonalité: {tonart}')
if jahr:
details.append(f'Jahr: {jahr}' if lang == 'DE' else f'Year: {jahr}' if lang == 'EN' else f'Année: {jahr}')
if besetzung:
details.append(f'Besetzung: {besetzung}' if lang == 'DE' else f'Instrumentation: {besetzung}' if lang == 'EN' else f'Instrumentation: {besetzung}')
if bemerkung:
details.append(f'<i>{bemerkung}</i>')
if details:
html += ' ' + ' &nbsp;|&nbsp; '.join(details) + '\n'
html += ' </td>\n'
html += '</tr>\n'
html += '<tr><td colspan="3"><hr noshade size="1" color="#DDBA86"></td></tr>\n'
return html
def generate_composer_catalog_content(composer, works, lang='DE'):
"""Generate the content HTML for a composer's catalog"""
composer_display = "Wieck-Schumann" if composer == "Wieck" else composer
content = f'''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-language" content="{lang.lower()}">
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1">
<title>Wolf's Thematic Index - {composer_display} Catalog</title>
<meta name="copyright" content="Documenta musica (C) 2005">
<link href="katalog.css" rel="stylesheet" type="text/css">
<script language="javascript">
<!--
function frameset() {{
if (!parent.unten)
location.href = "{lang.lower()}-katalog-{composer.lower()}.html";
}}
//-->
</script>
</head>
<body background="../bilder/hintergrund.gif" style="text-align: center;" onload="frameset()">
<div id="cont" align="center">
<table width="900" cellspacing="5" cellpadding="5">
<tr>
<td colspan="3" align="center">
<h1>{composer_display} - Werkverzeichnis</h1>
<p><i>{len(works)} {'Werke' if lang == 'DE' else 'Works' if lang == 'EN' else 'Œuvres'}</i></p>
</td>
</tr>
'''
# Sort works by work number
def safe_sort_key(w):
val = w.get('sorWerkNr', 0)
if val == '' or val == 'NULL' or val is None:
return 0
try:
return int(val)
except (ValueError, TypeError):
return 0
sorted_works = sorted(works, key=safe_sort_key)
for work in sorted_works:
content += generate_work_html(work, lang)
content += ''' </table>
</div>
</body>
</html>
'''
return content
def generate_composer_catalog_frameset(composer, lang='DE'):
"""Generate the frameset HTML for a composer's catalog"""
composer_display = "Wieck-Schumann" if composer == "Wieck" else composer
return f'''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1">
<meta http-equiv="content-language" content="{lang.lower()}">
<meta name="copyright" content="Documenta musica (C) 2005">
<link rel="shortcut icon" href="../bilder/favicon.ico" type="image/x-icon">
<title>Wolf's Thematic Index - {composer_display} Catalog</title>
<script language="javascript">
<!--
function framecall() {{
var adressanhang = location.search;
if (adressanhang)
frames.inhalt.location.href=adressanhang.substring(1,adressanhang.length);
}}
//-->
</script>
</head>
<frameset rows="60,*,60" border="0" frameborder="0" framespacing="0" onload="framecall()">
<frame name="oben" scrolling="no" noresize src="logo-oben.html">
<frame name="inhalt" scrolling="auto" noresize src="{lang.lower()}-katalog-{composer.lower()}-inhalt.html" target="_self">
<frame name="unten" scrolling="no" noresize target="inhalt" src="{lang.lower()}-katalog-menu.html">
<noframes>
<body>
<p>
Diese Seite verwendet Frames. Frames werden von Ihrem Browser aber nicht
unterstützt.
</p>
</body>
</noframes>
</frameset>
</html>
'''
def generate_catalog_index_content(lang='DE'):
"""Generate the catalog index content (composer selection)"""
content = f'''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-language" content="{lang.lower()}">
<title>Wolf's Thematic Index - Catalog</title>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-15">
<meta name="copyright" content="Documenta musica (C) 2005">
<script language="javascript">
<!--
function frameset() {{
if (!parent.unten)
location.href = "{lang.lower()}-katalog.html";
}}
//-->
</script>
<style type="text/css">
div {{ margin: 0; padding: 0; padding-top: 10px; }}
img {{ behavior: url(../js/iepngfix.htc); border: none; }}
</style>
</head>
<body background="../bilder/hintergrund.gif" style="text-align: center;" onload="frameset()">
<div style="padding-top: 36px;">
<a href="{lang.lower()}-katalog-bach.html" target="_parent">
<img src="../bilder/bach-blau-bio.png" width="372" height="40" alt="Johann Sebastian Bach"></a>
</div>
<div>
<a href="{lang.lower()}-katalog-beethoven.html" target="_parent">
<img src="../bilder/beethoven-blau-bio.png" width="324" height="42" alt="Ludwig van Beethoven"></a>
</div>
<div>
<a href="{lang.lower()}-katalog-brahms.html" target="_parent">
<img src="../bilder/brahms-blau-bio.png" width="269" height="40" alt="Johannes Brahms"></a>
</div>
<div>
<a href="{lang.lower()}-katalog-buxtehude.html" target="_parent">
<img src="../bilder/buxtehude-blau-bio.png" width="324" height="42" alt="Dieterich Buxtehude"></a>
</div>
<div>
<a href="{lang.lower()}-katalog-chopin.html" target="_parent">
<img src="../bilder/chopin-blau-bio.png" width="232" height="40" alt="Frédéric Chopin"></a>
</div>
<div>
<a href="{lang.lower()}-katalog-mozart.html" target="_parent">
<img src="../bilder/mozart-blau-bio.png" width="405" height="41" alt="Wolfgang Amadeus Mozart"></a>
</div>
<div>
<a href="{lang.lower()}-katalog-schumann.html" target="_parent">
<img src="../bilder/schumann-blau-bio.png" width="275" height="41" alt="Robert Schumann"></a>
</div>
<div>
<a href="{lang.lower()}-katalog-wagner.html" target="_parent">
<img src="../bilder/wagner-blau-bio.png" width="256" height="40" alt="Richard Wagner"></a>
</div>
<div>
<a href="{lang.lower()}-katalog-wieck.html" target="_parent">
<img src="../bilder/wieck-schumann-blau-bio.png" width="350" height="41" alt="Clara Wieck-Schumann"></a>
</div>
</body>
</html>
'''
return content
def generate_catalog_index_frameset(lang='DE'):
"""Generate the catalog index frameset"""
return f'''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1">
<meta http-equiv="content-language" content="{lang.lower()}">
<meta name="copyright" content="Documenta musica (C) 2005">
<link rel="shortcut icon" href="../bilder/favicon.ico" type="image/x-icon">
<title>Wolf's Thematic Index - Catalog</title>
<script language="javascript">
<!--
function framecall() {{
var adressanhang = location.search;
if (adressanhang)
frames.inhalt.location.href=adressanhang.substring(1,adressanhang.length);
}}
//-->
</script>
</head>
<frameset rows="60,*,60" border="0" frameborder="0" framespacing="0" onload="framecall()">
<frame name="oben" scrolling="no" noresize src="logo-oben.html">
<frame name="inhalt" scrolling="auto" noresize src="{lang.lower()}-katalog-inhalt.html" target="_self">
<frame name="unten" scrolling="no" noresize target="inhalt" src="{lang.lower()}-katalog-menu.html">
<noframes>
<body>
<p>
Diese Seite verwendet Frames. Frames werden von Ihrem Browser aber nicht
unterstützt.
</p>
</body>
</noframes>
</frameset>
</html>
'''
def generate_catalog_menu(lang='DE'):
"""Generate the catalog menu"""
menu_labels = {
'DE': {'start': 'Start', 'intro': 'Einführung', 'bio': 'Biographien',
'katalog': 'Katalog', 'form': 'Suchformular', 'klav': 'Klaviatur',
'quellen': 'Quellen', 'impressum': 'Impressum'},
'EN': {'start': 'Start', 'intro': 'Introduction', 'bio': 'Biographies',
'katalog': 'Catalog', 'form': 'Search Form', 'klav': 'Keyboard',
'quellen': 'Sources', 'impressum': 'Imprint'},
'FR': {'start': 'Start', 'intro': 'Introduction', 'bio': 'Biographies',
'katalog': 'Catalogue', 'form': 'Formulaire', 'klav': 'Clavier',
'quellen': 'Sources', 'impressum': 'Mentions légales'}
}
labels = menu_labels[lang]
return f'''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-language" content="{lang.lower()}">
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1">
<meta name="copyright" content="Documenta musica (C) 2005">
<meta name="robots" content="noindex">
<title>Wolf's Thematic Index - Catalog - Menu</title>
<base target="inhalt">
<link href="menu.css" rel="stylesheet" type="text/css">
</head>
<body>
<table>
<tr>
<td>
<a href="../index.html" target="_parent">
{labels['start']}</td>
<td>
<a href="{lang.lower()}-intro.html" target="_parent">
{labels['intro']}</td>
<td>
<a href="{lang.lower()}-bio.html" target="_parent">
{labels['bio']}</a></td>
<td id="td_selected">
<a href="{lang.lower()}-katalog.html" target="_parent" id="a_selected">
{labels['katalog']}</a></td>
<td>
<a href="{lang.lower()}-formular.html" target="_parent">
{labels['form']}</a></td>
<td>
<a href="{lang.lower()}-klaviatur.html" target="_parent">
{labels['klav']}</a></td>
<td>
<a href="{lang.lower()}-quellen.html" target="_parent">
{labels['quellen']}</a></td>
<td>
<a href="{lang.lower()}-impressum.html" target="_parent">
{labels['impressum']}</a></td>
</tr>
</table>
</body>
</html>
'''
def generate_katalog_css():
"""Generate the katalog.css file"""
return '''/* Wolf's Thematic Index of the Works of the Great Composers */
/* katalog.css */
body, table, tr, td {
font-size: 1em;
font-family: georgia, 'times new roman', times, serif;
line-height: 130%;
margin-top: 0px;
margin-bottom: 0px;
}
p {
margin-top: 0px;
margin-bottom:5px;
padding: 0;
}
h1 {
font-size: 200%;
margin-top: 10px;
margin-bottom: 5px;
}
h2 {
font-size: 150%;
margin-top: 10px;
margin-bottom: 5px;
}
a {
font-weight: bold;
color: #0000ff;
font-size: 1em;
letter-spacing: 0.05em;
text-decoration: none;
}
a:hover {
background-color: Yellow;
}
table {
border: none;
}
tr {
margin: 5px 0;
}
td {
padding: 3px 5px;
}
hr {
color: #DDBA86;
background-color: #DDBA86;
}
'''
def main():
"""Main function to generate all catalog files"""
base_dir = Path(__file__).parent
sql_file = base_dir / 'db-dump' / 'initial_db.sql'
html_dir = base_dir / 'src' / 'html'
# Parse SQL dump
all_works = parse_sql_dump(sql_file)
# Group works by composer
works_by_composer = {}
for work in all_works:
composer = work['Komponist']
if composer not in works_by_composer:
works_by_composer[composer] = []
works_by_composer[composer].append(work)
print(f"\nGenerating catalog pages...")
# Generate catalog CSS
print("Creating katalog.css...")
css_file = html_dir / 'katalog.css'
css_file.write_text(generate_katalog_css(), encoding='iso-8859-1', errors='replace')
# Generate for each language
for lang in ['DE', 'EN', 'FR']:
print(f"\nGenerating {lang} catalog files...")
# Generate catalog index files
frameset = generate_catalog_index_frameset(lang)
content = generate_catalog_index_content(lang)
menu = generate_catalog_menu(lang)
(html_dir / f'{lang.lower()}-katalog.html').write_text(frameset, encoding='iso-8859-1', errors='replace')
(html_dir / f'{lang.lower()}-katalog-inhalt.html').write_text(content, encoding='iso-8859-1', errors='replace')
(html_dir / f'{lang.lower()}-katalog-menu.html').write_text(menu, encoding='iso-8859-1', errors='replace')
# Generate composer catalog files
for composer in COMPOSERS:
if composer not in works_by_composer:
print(f" Warning: No works found for {composer}")
continue
works = works_by_composer[composer]
print(f" Generating {composer} catalog ({len(works)} works)...")
frameset = generate_composer_catalog_frameset(composer, lang)
content = generate_composer_catalog_content(composer, works, lang)
(html_dir / f'{lang.lower()}-katalog-{composer.lower()}.html').write_text(frameset, encoding='iso-8859-1', errors='replace')
(html_dir / f'{lang.lower()}-katalog-{composer.lower()}-inhalt.html').write_text(content, encoding='iso-8859-1', errors='replace')
print("\n✅ Catalog generation complete!")
print(f" Generated {3 * (3 + 2 * len(COMPOSERS)) + 1} files")
print(f" - 1 CSS file")
print(f" - 9 index files (3 languages × 3 files each)")
print(f" - {2 * len(COMPOSERS) * 3} composer files ({len(COMPOSERS)} composers × 2 files × 3 languages)")
if __name__ == '__main__':
main()