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 (QR)

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

(no files)
` w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(Page("/applovin", "AppLovin Upload", body))) } func ApplovinPick(w http.ResponseWriter, r *http.Request) { picked := PickFiles( "Select HTML files", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, true, "", ) 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 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" }