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) {
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 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"
}