#!/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 = '\n' html += f' \n' # Work number if werknr: html += f' {werknr}\n' else: html += ' \n' # Work details html += ' \n' if title: html += f' {title}
\n' if incipit: html += f' {incipit}
\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'{bemerkung}') if details: html += ' ' + '  |  '.join(details) + '\n' html += ' \n' html += '\n' html += '
\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''' Wolf's Thematic Index - {composer_display} Catalog
''' # 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 += '''

{composer_display} - Werkverzeichnis

{len(works)} {'Werke' if lang == 'DE' else 'Works' if lang == 'EN' else 'Œuvres'}

''' 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''' Wolf's Thematic Index - {composer_display} Catalog <body> <p> Diese Seite verwendet Frames. Frames werden von Ihrem Browser aber nicht unterstützt. </p> </body> ''' def generate_catalog_index_content(lang='DE'): """Generate the catalog index content (composer selection)""" content = f''' Wolf's Thematic Index - Catalog
Johann Sebastian Bach
Ludwig van Beethoven
Johannes Brahms
Dieterich Buxtehude
Frédéric Chopin
Wolfgang Amadeus Mozart
Robert Schumann
Richard Wagner
Clara Wieck-Schumann
''' return content def generate_catalog_index_frameset(lang='DE'): """Generate the catalog index frameset""" return f''' Wolf's Thematic Index - Catalog <body> <p> Diese Seite verwendet Frames. Frames werden von Ihrem Browser aber nicht unterstützt. </p> </body> ''' 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''' Wolf's Thematic Index - Catalog - Menu
{labels['start']} {labels['intro']} {labels['bio']} {labels['katalog']} {labels['form']} {labels['klav']} {labels['quellen']} {labels['impressum']}
''' 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()