File size: 5,753 Bytes
0403886
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52ad754
 
 
 
 
0403886
52ad754
0403886
 
 
52ad754
 
 
 
 
 
 
 
0403886
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52ad754
0403886
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52ad754
 
0403886
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52ad754
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#!/usr/bin/env python3
"""
Gradio app to convert one or more MIDI files into Geometry Dash .gmd files
(using midi_to_gd.py). For each uploaded MIDI the app will create a .gmd
file (plist wrapper with compressed k4 level string) and return a zip
containing all generated .gmd files.
"""
import os
import sys
import tempfile
import zipfile
import traceback
from typing import List, Tuple

HERE = os.path.dirname(os.path.abspath(__file__))
if HERE not in sys.path:
    sys.path.insert(0, HERE)

try:
    import gradio as gr
except Exception as e:
    raise RuntimeError("Gradio is required. Install with: pip install gradio") from e

# Import functions from the provided script
try:
    from midi_to_gd import (
        parse_midi_notes,
        generate_gd_level_objects,
        build_full_level_string,
        encrypt_level_string,
    )
except Exception as e:
    raise RuntimeError("Failed to import from midi_to_gd.py. Ensure the file is in the same directory.") from e


def _get_local_path_from_gr_file(f) -> str:
    """
    Accepts the various shapes Gradio may pass:
      - a filesystem path string
      - a dict like {'name': '/tmp/....'}
      - a list of the above (we only call this per-file)
    Returns a single filesystem path string or empty if not found.
    """
    if not f:
        return ""
    if isinstance(f, str):
        return f
    if isinstance(f, (list, tuple)) and f:
        # take first element
        return _get_local_path_from_gr_file(f[0])
    if isinstance(f, dict):
        # Gradio sometimes returns {'name': '/tmp/...'}
        return f.get("name") or f.get("file") or ""
    # Fallback: try attribute .name
    return getattr(f, "name", "")


def convert_midis_to_zip(midi_files: List, beats_scale: float = 2.0, name_prefix: str = "") -> Tuple[str, str]:
    """
    Process uploaded midi files (list), generate .gmd files for each, and return
    a path to a zip archive and a status text message.

    Returns:
      (zip_file_path, status_message)
    """
    try:
        if not midi_files:
            return None, "No MIDI files uploaded."

        out_dir = tempfile.mkdtemp(prefix="gd_midis_")
        created_files = []

        # midi_files is expected to be a list of file paths (type="filepath")
        for idx, f in enumerate(midi_files, start=1):
            midi_path = _get_local_path_from_gr_file(f)
            if not os.path.exists(midi_path):
                return None, f"Uploaded file not found on disk: {midi_path}"

            base = os.path.splitext(os.path.basename(midi_path))[0]
            level_name = f"{name_prefix}{'_' if name_prefix else ''}{base}" if name_prefix else base

            notes = parse_midi_notes(midi_path)
            raw_objects = generate_gd_level_objects(notes, blocks_per_beat=beats_scale)

            full_level_data = build_full_level_string(raw_objects)
            encrypted_k4 = encrypt_level_string(full_level_data)

            gmd_content = f"""<?xml version="1.0"?>
<plist version="1.0" gjver="2.0">
    <dict>
        <key>kCEK</key><i>4</i>
        <key>k2</key><s>{level_name}</s>
        <key>k3</key><s>MIDI converted Piano Roll</s>
        <key>k4</key><s>{encrypted_k4}</s>
        <key>k5</key><s>Player</s>
    </dict>
</plist>"""

            out_path = os.path.join(out_dir, f"{level_name}.gmd")
            with open(out_path, "w", encoding="utf-8") as wf:
                wf.write(gmd_content)
            created_files.append(out_path)

        zip_path = os.path.join(out_dir, "gd_levels.zip")
        with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
            for p in created_files:
                zf.write(p, arcname=os.path.basename(p))

        status_lines = [
            f"Generated {len(created_files)} .gmd file(s).",
            f"ZIP archive: {os.path.basename(zip_path)}",
            "Files:",
        ] + [f"- {os.path.basename(p)}" for p in created_files]

        return zip_path, "\n".join(status_lines)
    except Exception:
        tb = traceback.format_exc()
        return None, f"Error during conversion:\n{tb}"


def launch_ui():
    with gr.Blocks() as demo:
        gr.Markdown("# MIDI → Geometry Dash (.gmd) Converter")
        gr.Markdown(
            "Upload one or more MIDI files. The app will convert each MIDI into a Geometry Dash .gmd "
            "file (plist wrapper with compressed level string) and return a ZIP archive containing all generated .gmd files."
        )

        with gr.Row():
            # NOTE: type="filepath" is the correct modern API. 'file' is invalid.
            midi_input = gr.File(label="Upload MIDI files (.mid/.midi)", file_count="multiple", type="filepath")
            settings_col = gr.Column(scale=1)
            beats_scale = gr.Number(value=2.0, label="Blocks per beat (blocks/quarter-note)", precision=2)
            name_prefix = gr.Textbox(label="Optional level name prefix", placeholder="Prefix (optional)")
        convert_btn = gr.Button("Convert and Download ZIP")

        output_zip = gr.File(label="Download ZIP of .gmd files")
        status = gr.Textbox(label="Status", interactive=False, lines=6)

        def _on_click(files, beats, prefix):
            zip_path, status_text = convert_midis_to_zip(files, beats, prefix)
            return zip_path, status_text

        convert_btn.click(fn=_on_click, inputs=[midi_input, beats_scale, name_prefix], outputs=[output_zip, status])

        gr.Markdown("Notes: mido is required to parse MIDI files. Install with `pip install mido`.")
        gr.Markdown("If the app cannot find your local CCLocalLevels.dat or you just want the .gmd files, use the ZIP to import them with your editor.")

    demo.launch()


if __name__ == "__main__":
    launch_ui()