package main import ( "bytes" "encoding/json" "fmt" "io" "math/rand" "mime/multipart" "net/http" urlpkg "net/url" "os" "path/filepath" "regexp" "strings" "time" ) var applovinUserAgents = []string{ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", } func pickApplovinUserAgent(randomize bool) string { if !randomize { return applovinUserAgents[0] } return applovinUserAgents[rand.Intn(len(applovinUserAgents))] } func ApplovinPage(w http.ResponseWriter, r *http.Request) { body := `

AppLovin Playable Preview

Upload HTML files to p.applov.in and get QR codes to scan with the AppLovin Playable Preview app.

Inputs

or drop HTML files or folders here
(no files selected)
Expanded QR
` w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(Page("/applovin", "AppLovin Playable Preview", body))) } func ApplovinPick(w http.ResponseWriter, r *http.Request) { cfg := LoadConfig() picked := PickFiles( "Select HTML files", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, true, cfg.LastPickDir, ) if len(picked) > 0 { cfg.LastPickDir = filepath.Dir(picked[0]) _ = SaveConfig(cfg) } files := make([]map[string]string, 0, len(picked)) for _, p := range picked { files = append(files, map[string]string{"path": p, "name": filepath.Base(p)}) } writeJSON(w, map[string]any{"files": files}) } func ApplovinPickFolder(w http.ResponseWriter, r *http.Request) { cfg := LoadConfig() dir := PickFolder("Select folder", cfg.LastPickDir) if dir == "" { writeJSON(w, map[string]any{"files": []any{}}) return } cfg.LastPickDir = dir _ = SaveConfig(cfg) paths := collectHTMLFiles(dir) files := make([]map[string]string, 0, len(paths)) for _, p := range paths { files = append(files, map[string]string{"path": p, "name": filepath.Base(p)}) } writeJSON(w, map[string]any{"files": files}) } func ApplovinUpload(w http.ResponseWriter, r *http.Request) { var req struct { Paths []string `json:"paths"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } cfg := LoadConfig().Applovin cookie := cfg.Cookie delayMs := cfg.DelayMs if delayMs < 0 { delayMs = 0 } randomizeUA := true if cfg.RandomizeUserAgent != nil { randomizeUA = *cfg.RandomizeUserAgent } w.Header().Set("Content-Type", "application/x-ndjson") flusher, _ := w.(http.Flusher) send := func(obj any) { data, _ := json.Marshal(obj) _, _ = w.Write(append(data, '\n')) if flusher != nil { flusher.Flush() } } htmlRx := regexp.MustCompile(`(?i)\.html?$`) buildForm := func(filePath string) (*bytes.Buffer, string, error) { data, err := os.ReadFile(filePath) if err != nil { return nil, "", err } var buf bytes.Buffer mw := multipart.NewWriter(&buf) fw, err := createFormFileWithType(mw, "file", filepath.Base(filePath), "text/html") if err != nil { return nil, "", err } if _, err := fw.Write(data); err != nil { return nil, "", err } if err := mw.Close(); err != nil { return nil, "", err } return &buf, mw.FormDataContentType(), nil } doPost := func(url string, body *bytes.Buffer, contentType string) (int, string, error) { req, err := http.NewRequest("POST", url, body) if err != nil { return 0, "", err } req.Header.Set("Content-Type", contentType) req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01") req.Header.Set("Origin", "https://p.applov.in") req.Header.Set("Referer", "https://p.applov.in/playablePreview?create=1&qr=1") req.Header.Set("X-Requested-With", "XMLHttpRequest") req.Header.Set("User-Agent", pickApplovinUserAgent(randomizeUA)) if cookie != "" { req.Header.Set("Cookie", "__Host-APPLOVINID="+cookie) } resp, err := http.DefaultClient.Do(req) if err != nil { return 0, "", err } defer resp.Body.Close() out, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(out), nil } for i, filePath := range req.Paths { fileName := filepath.Base(filePath) idx := i + 1 fileErr := func(msg string) { send(map[string]any{"type": "fileError", "name": fileName, "message": msg}) } if filePath == "" || !fileExists(filePath) { fileErr("File missing") continue } if !htmlRx.MatchString(filePath) { fileErr("Must be .html") continue } if i > 0 && delayMs > 0 { send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Waiting %dms before %s...", idx, len(req.Paths), delayMs, fileName)}) time.Sleep(time.Duration(delayMs) * time.Millisecond) } send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Validating %s...", idx, len(req.Paths), fileName)}) body, ct, err := buildForm(filePath) if err != nil { fileErr("Read failed: " + err.Error()) continue } status, respText, err := doPost("https://p.applov.in/validateHTMLContent", body, ct) if err != nil { fileErr("Network (validate): " + err.Error()) continue } if status < 200 || status >= 300 { fileErr(fmt.Sprintf("Validate HTTP %d: %s", status, truncate(respText, 300))) continue } var vJSON map[string]any _ = json.Unmarshal([]byte(respText), &vJSON) if code, ok := vJSON["code"]; ok { if codeNum, isNum := code.(float64); isNum && codeNum != 200 { fileErr("Validation failed: " + truncate(respText, 300)) continue } } send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Uploading %s...", idx, len(req.Paths), fileName)}) body, ct, err = buildForm(filePath) if err != nil { fileErr("Read failed: " + err.Error()) continue } status, respText, err = doPost("https://p.applov.in/getCachedAdURL", body, ct) if err != nil { fileErr("Network (cache): " + err.Error()) continue } if status < 200 || status >= 300 { fileErr(fmt.Sprintf("Cache HTTP %d: %s", status, truncate(respText, 300))) continue } var cJSON map[string]any if err := json.Unmarshal([]byte(respText), &cJSON); err != nil { fileErr("Non-JSON: " + truncate(respText, 300)) continue } hash, _ := cJSON["results"].(string) if hash == "" { fileErr("No hash in response: " + truncate(respText, 300)) continue } send(map[string]any{ "type": "fileResult", "name": fileName, "hash": hash, "previewUrl": "https://p.applov.in/getPreviewHTML?preview=" + hash, "pageUrl": "https://p.applov.in/playablePreview?preview=" + hash + "&qr=1", "qrImg": "https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=" + urlpkg.QueryEscape(hash), }) } send(map[string]any{"type": "done"}) } func qrBaseName(name string) string { base := name if base == "" { base = "qr" } base = regexp.MustCompile(`(?i)\.html?$`).ReplaceAllString(base, "") base = regexp.MustCompile(`[\\/:*?"<>|]`).ReplaceAllString(base, "_") if base == "" { base = "qr" } return base } func downloadQR(url, destPath string) error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("HTTP %d", resp.StatusCode) } data, err := io.ReadAll(resp.Body) if err != nil { return err } return os.WriteFile(destPath, data, 0644) } func ApplovinSaveQR(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` URL string `json:"url"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]any{"ok": false, "message": err.Error()}) return } if req.URL == "" { writeJSON(w, map[string]any{"ok": false, "message": "Missing URL"}) return } base := qrBaseName(req.Name) dest := PickSaveFile("Save QR", base+".png", []FileFilter{{Name: "PNG Image", Extensions: []string{"png"}}}, "") if dest == "" { writeJSON(w, map[string]any{"ok": false, "message": "Cancelled"}) return } if err := downloadQR(req.URL, dest); err != nil { writeJSON(w, map[string]any{"ok": false, "message": "Save failed: " + err.Error()}) return } writeJSON(w, map[string]any{"ok": true, "message": "Saved QR: " + dest}) } func ApplovinSaveAllQRs(w http.ResponseWriter, r *http.Request) { var req struct { Items []struct { Name string `json:"name"` URL string `json:"url"` } `json:"items"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]any{"ok": false, "message": err.Error()}) return } if len(req.Items) == 0 { writeJSON(w, map[string]any{"ok": false, "message": "No items"}) return } dir := PickFolder("Choose folder to save QRs", "") if dir == "" { writeJSON(w, map[string]any{"ok": false, "message": "Cancelled"}) return } saved := 0 errs := []string{} for _, it := range req.Items { base := qrBaseName(it.Name) dest := filepath.Join(dir, base+".png") n := 1 for fileExists(dest) { dest = filepath.Join(dir, fmt.Sprintf("%s (%d).png", base, n)) n++ } if err := downloadQR(it.URL, dest); err != nil { errs = append(errs, it.Name+": "+err.Error()) continue } saved++ } if len(errs) > 0 { writeJSON(w, map[string]any{ "ok": false, "message": fmt.Sprintf("Saved %d/%d. Errors: %s", saved, len(req.Items), strings.Join(errs, "; ")), }) return } writeJSON(w, map[string]any{ "ok": true, "message": fmt.Sprintf("Saved %d QR%s to %s", saved, plural(saved), dir), }) } func plural(n int) string { if n == 1 { return "" } return "s" }