nyt-to-xd.sh (4907B)
1 #!/usr/bin/env bash 2 # 3 # Convert a NYT puzzle JSON to XD using the app's NYTToXDConverter. 4 # 5 # Usage: 6 # Scripts/nyt-to-xd.sh --json <path> # convert an existing file 7 # Scripts/nyt-to-xd.sh --date YYYY-MM-DD # fetch then convert 8 # [--output <path>] # default: stdout 9 10 set -euo pipefail 11 12 repo_root="$(cd "$(dirname "$0")/.." && pwd)" 13 fetch_script="${repo_root}/Scripts/fetch-nyt.sh" 14 15 # The converter's slice of the app target, compiled from the real sources rather 16 # than mirrored into a stub. This used to hand-copy XD's constants to avoid 17 # building the whole app, but the converter now also writes the `## Decorations` 18 # section, which pulls in Puzzle and its neighbours — and a stub of those would 19 # drift silently. These seven files are self-contained (nothing here reaches into 20 # Core Data or CloudKit), so compiling them directly costs a second and can't go 21 # stale. 22 sources=( 23 "${repo_root}/Crossmate/Models/XD.swift" 24 "${repo_root}/Crossmate/Models/XDDecorationWriter.swift" 25 "${repo_root}/Crossmate/Models/XDMarkup.swift" 26 "${repo_root}/Crossmate/Models/Puzzle.swift" 27 "${repo_root}/Crossmate/Models/PlayerSelection.swift" 28 "${repo_root}/Crossmate/Models/GridPosition.swift" 29 "${repo_root}/Crossmate/Services/NYTOverlaySlicer.swift" 30 "${repo_root}/Crossmate/Services/NYTOverlayLetterRecognizer.swift" 31 "${repo_root}/Crossmate/Services/NYTToXDConverter.swift" 32 ) 33 for source in "${sources[@]}"; do 34 if [[ ! -f "$source" ]]; then 35 echo "error: missing source file: $source" >&2 36 exit 1 37 fi 38 done 39 40 json_path="" 41 date_arg="" 42 output_path="" 43 44 while [[ $# -gt 0 ]]; do 45 case "$1" in 46 --json) json_path="$2"; shift 2 ;; 47 --date) date_arg="$2"; shift 2 ;; 48 --output) output_path="$2"; shift 2 ;; 49 -h|--help) 50 sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//' 51 exit 0 52 ;; 53 *) echo "error: unknown arg '$1'" >&2; exit 2 ;; 54 esac 55 done 56 57 if [[ -z "$json_path" && -z "$date_arg" ]]; then 58 echo "error: pass --json <path> or --date YYYY-MM-DD" >&2 59 exit 2 60 fi 61 if [[ -n "$json_path" && -n "$date_arg" ]]; then 62 echo "error: --json and --date are mutually exclusive" >&2 63 exit 2 64 fi 65 66 tmp_dir="$(mktemp -d)" 67 trap 'rm -rf "$tmp_dir"' EXIT 68 69 if [[ -n "$date_arg" ]]; then 70 json_path="${tmp_dir}/nyt-${date_arg}.json" 71 bash "$fetch_script" "$date_arg" "$json_path" >/dev/null 72 fi 73 74 if [[ ! -f "$json_path" ]]; then 75 echo "error: JSON file not found: $json_path" >&2 76 exit 1 77 fi 78 79 # Fetch both overlay phases, when present, so the converted .xd carries the 80 # same baked-in artwork the app would produce. Asset hosts need no cookie. 81 before_start_path="" 82 before_start_uri="$(jq -r ' 83 (.body[0].overlays.beforeStart // empty) as $i 84 | if ($i | type) == "number" then (.assets[$i - 1].uri // empty) else empty end 85 ' "$json_path" 2>/dev/null || true)" 86 if [[ -n "$before_start_uri" ]]; then 87 before_start_path="${tmp_dir}/before-start.png" 88 if ! curl -sSfL "$before_start_uri" -o "$before_start_path"; then 89 echo "warning: could not fetch overlay ${before_start_uri}; converting without it" >&2 90 before_start_path="" 91 fi 92 fi 93 94 after_solve_path="" 95 after_solve_uri="$(jq -r ' 96 (.body[0].overlays.afterSolve // empty) as $i 97 | if ($i | type) == "number" then (.assets[$i - 1].uri // empty) else empty end 98 ' "$json_path" 2>/dev/null || true)" 99 if [[ -n "$after_solve_uri" ]]; then 100 after_solve_path="${tmp_dir}/after-solve.png" 101 if ! curl -sSfL "$after_solve_uri" -o "$after_solve_path"; then 102 echo "warning: could not fetch overlay ${after_solve_uri}; converting without it" >&2 103 after_solve_path="" 104 fi 105 fi 106 107 driver="${tmp_dir}/main.swift" 108 cat > "$driver" <<'SWIFT' 109 import Foundation 110 111 guard CommandLine.arguments.count >= 2 else { 112 FileHandle.standardError.write(Data("error: missing JSON path\n".utf8)) 113 exit(2) 114 } 115 let url = URL(fileURLWithPath: CommandLine.arguments[1]) 116 let data = try Data(contentsOf: url) 117 var beforeStartImage: Data? = nil 118 if CommandLine.arguments.count >= 3, !CommandLine.arguments[2].isEmpty { 119 beforeStartImage = try Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[2])) 120 } 121 var afterSolveImage: Data? = nil 122 if CommandLine.arguments.count >= 4, !CommandLine.arguments[3].isEmpty { 123 afterSolveImage = try Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[3])) 124 } 125 let xd = try NYTToXDConverter.convert( 126 jsonData: data, 127 beforeStartImage: beforeStartImage, 128 afterSolveImage: afterSolveImage 129 ) 130 print(xd) 131 SWIFT 132 133 binary="${tmp_dir}/nyt-to-xd" 134 swiftc -O -swift-version 6 "${sources[@]}" "$driver" -o "$binary" 135 136 if [[ -n "$output_path" ]]; then 137 "$binary" "$json_path" "$before_start_path" "$after_solve_path" > "$output_path" 138 echo "wrote $output_path" >&2 139 else 140 "$binary" "$json_path" "$before_start_path" "$after_solve_path" 141 fi