package main import ( "archive/zip" "encoding/json" "fmt" "net/http" "os" "path/filepath" "regexp" "strings" ) type playworksConvertOptions struct { SourcePath string `json:"sourcePath"` OutputDir string `json:"outputDir"` BaseName string `json:"baseName"` Networks []string `json:"networks"` } type playworksNetworkResult struct { Network string `json:"network"` File string `json:"file"` OK bool `json:"ok"` Error string `json:"error,omitempty"` } func PlayworksPage(w http.ResponseWriter, r *http.Request) { body := `

Playworks Converter

Convert a Playworks HTML export into network-specific playable outputs.

Inputs

Source HTML (Playworks export)
Base Filename
Output Folder
Networks to generate
` w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(Page("/playworks", "Playworks Converter", body))) } func PlayworksPickSource(w http.ResponseWriter, r *http.Request) { picked := PickFiles("Select Playworks HTML", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, false, "") if len(picked) == 0 { writeJSON(w, map[string]any{}) return } writeJSON(w, map[string]any{"path": picked[0], "dir": filepath.Dir(picked[0]), "baseName": playworksSourceBaseName(picked[0])}) } func PlayworksPickOutput(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]any{"path": PickFolder("Select Output Folder", "")}) } func PlayworksConvert(w http.ResponseWriter, r *http.Request) { var opts playworksConvertOptions if err := json.NewDecoder(r.Body).Decode(&opts); err != nil { writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": err.Error()}) return } if !fileExists(opts.SourcePath) { writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Source file not found."}) return } if err := os.MkdirAll(opts.OutputDir, 0755); err != nil { writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Could not create output folder."}) return } data, err := os.ReadFile(opts.SourcePath) if err != nil { writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Could not read source: " + err.Error()}) return } results := []playworksNetworkResult{} for _, network := range opts.Networks { filePath, err := convertPlayworksNetwork(string(data), opts, network) if err != nil { results = append(results, playworksNetworkResult{Network: network, OK: false, Error: err.Error()}) continue } results = append(results, playworksNetworkResult{Network: network, File: filePath, OK: true}) } writeJSON(w, map[string]any{"results": results}) } var playworksSuffixRx = regexp.MustCompile(`_(?:un|al|is|fb|gg|mo|vu|mtg|tt)$`) func playworksSourceBaseName(sourcePath string) string { base := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath)) return playworksSuffixRx.ReplaceAllString(base, "") } func playworksOutputBaseName(opts playworksConvertOptions) string { baseName := strings.TrimSpace(opts.BaseName) if baseName == "" { baseName = playworksSourceBaseName(opts.SourcePath) } baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName)) baseName = filepath.Base(baseName) baseName = strings.Map(func(r rune) rune { switch r { case '<', '>', ':', '"', '/', '\\', '|', '?', '*': return '_' default: return r } }, baseName) baseName = strings.TrimSpace(baseName) if baseName == "" { return playworksSourceBaseName(opts.SourcePath) } return baseName } func convertPlayworksNetwork(htmlSrc string, opts playworksConvertOptions, network string) (string, error) { transformed := transformPlayworksHTML(htmlSrc, network) baseName := playworksOutputBaseName(opts) htmlFileName := fmt.Sprintf("%s_%s.html", baseName, network) outputDir := filepath.Join(opts.OutputDir, playworksNetworkOutputFolder(network)) if err := os.MkdirAll(outputDir, 0755); err != nil { return "", err } if playworksNeedsZip(network) { zipPath := filepath.Join(outputDir, fmt.Sprintf("%s_%s.zip", baseName, network)) if err := createSingleFileZip([]byte(transformed), "index.html", zipPath); err != nil { return "", err } return zipPath, nil } outPath := filepath.Join(outputDir, htmlFileName) if err := os.WriteFile(outPath, []byte(transformed), 0644); err != nil { return "", err } return outPath, nil } func playworksNetworkOutputFolder(network string) string { switch network { case "al": return "Applovin" case "is": return "Ironsource" case "un": return "Unity" case "fb": return "Facebook" case "gg": return "GoogleAds" case "mo": return "Moloco" case "vu": return "Vungle" case "mtg": return "Mintegral" case "tt": return "TikTok" default: return network } } func playworksNeedsZip(network string) bool { return network == "gg" || network == "vu" || network == "mtg" } func transformPlayworksHTML(htmlSrc, network string) string { result := htmlSrc headInject := buildPlayworksLifecycleScript() if network == "al" || network == "is" { headInject += `` } else if network == "gg" { headInject += `` } result = strings.Replace(result, "", headInject+"\n", 1) if network == "vu" { result = strings.Replace(result, "", ` `, 1) } else if network == "mtg" { result = strings.Replace(result, "", ``, 1) } else if network == "tt" { result = strings.Replace(result, "", ` `, 1) } result = replacePlayworksCTAScript(result, network) if network == "un" { result = strings.ReplaceAll(result, "window.top", "window.self") } return result } func replacePlayworksCTAScript(htmlSrc, network string) string { marker := "Luna.Unity.Playable.InstallFullGame=function" markerIdx := strings.LastIndex(htmlSrc, marker) if markerIdx == -1 { return htmlSrc } scriptOpenIdx := strings.LastIndex(htmlSrc[:markerIdx], "") if scriptCloseRel == -1 { return htmlSrc } scriptCloseIdx := markerIdx + scriptCloseRel return htmlSrc[:scriptOpenIdx] + buildPlayworksConsoleRestoreScript() + buildPlayworksCTAScript(network) + htmlSrc[scriptCloseIdx+len(""):] } func buildPlayworksConsoleRestoreScript() string { return strings.Join([]string{ ``, }, "") } func buildPlayworksLifecycleScript() string { return strings.Join([]string{ ``, }, "") } func buildPlayworksCTAScript(network string) string { urlSetup := `n=n||window.$environment.packageConfig.iosLink,i=i||window.$environment.packageConfig.androidLink;var o=/iphone|ipad|ipod|macintosh/i.test(window.navigator.userAgent.toLowerCase())?n:i;` closeCall := `try{window.gameClose();}catch(e){}` ctaLogic := closeCall + `window.open(o,"_blank");` switch network { case "al", "is": ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){if(typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",function(){mraid.open(o)});}else{mraid.open(o);}}else{window.open(o,"_blank");}` case "un": ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){mraid.open(o);}else{window.open(o,"_blank");}` case "fb": ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");` case "gg": ctaLogic = closeCall + `if(typeof ExitApi!=="undefined"&&typeof ExitApi.exit==="function"){ExitApi.exit();return;}window.open(o,"_blank");` case "mo": ctaLogic = closeCall + `if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");` case "vu": ctaLogic = closeCall + `try{parent.postMessage("download","*");}catch(e){}` case "mtg": ctaLogic = closeCall + `if(typeof window.install==="function"){window.install();return;}window.open(o,"_blank");` case "tt": ctaLogic = closeCall + `if(typeof window.openAppStore==="function"){window.openAppStore();return;}window.open(o,"_blank");` } return `` } func createSingleFileZip(data []byte, nameInZip, destZipPath string) error { _ = os.Remove(destZipPath) out, err := os.Create(destZipPath) if err != nil { return err } defer out.Close() zw := zip.NewWriter(out) entry, err := zw.Create(nameInZip) if err != nil { _ = zw.Close() return err } if _, err := entry.Write(data); err != nil { _ = zw.Close() return err } return zw.Close() }