Add outputPath parameter to download and checkVersionInDownloaded functions to allow specifying the download directory. Update main function to pass the outputPath parameter to these functions.
313 lines
7.2 KiB
Go
313 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
func authInfo(host string, port int, user, pswd string) bool {
|
|
linkAuthInfo := fmt.Sprintf(
|
|
"http://%s:%d/auth/info?user=%s&pswd=%v", host, port, user, pswd,
|
|
)
|
|
urlAuthInfo, err := url.Parse(linkAuthInfo)
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(3 * time.Second)
|
|
return false
|
|
}
|
|
resp, err := http.Get(urlAuthInfo.String())
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(3 * time.Second)
|
|
return false
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(3 * time.Second)
|
|
return false
|
|
}
|
|
var result map[string]any
|
|
err = json.Unmarshal(body, &result)
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(3 * time.Second)
|
|
return false
|
|
}
|
|
var resultAuthInfo map[string]any
|
|
err = json.Unmarshal(body, &resultAuthInfo)
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(3 * time.Second)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func authLogin(host string, port int, user, pswd string) bool {
|
|
linkAuthLogin := fmt.Sprintf(
|
|
"http://%s:%d/auth/login?user=%s&pswd=%v", host, port, user, pswd,
|
|
)
|
|
|
|
urlAuthLogin, err := url.Parse(linkAuthLogin)
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(10 * time.Second)
|
|
return false
|
|
}
|
|
|
|
resp, err := http.Get(urlAuthLogin.String())
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(10 * time.Second)
|
|
return false
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(10 * time.Second)
|
|
return false
|
|
}
|
|
|
|
var result map[string]any
|
|
err = json.Unmarshal(body, &result)
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(10 * time.Second)
|
|
return false
|
|
}
|
|
|
|
success, ok := result["success"].(bool)
|
|
if !ok || !success {
|
|
fmt.Println("login failed, retrying...", result)
|
|
time.Sleep(10 * time.Second)
|
|
return false
|
|
}
|
|
|
|
fmt.Println("login successful")
|
|
return true
|
|
|
|
}
|
|
|
|
type App struct {
|
|
BundleID string `json:"bundleID"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
func search(host string, port int, query string, limit int) ([]App, error) {
|
|
linkSearch := fmt.Sprintf(
|
|
"http://%s:%d/search?query=%s&limit=%d", host, port, query, limit,
|
|
)
|
|
urlSearch, err := url.Parse(linkSearch)
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(3 * time.Second)
|
|
return nil, err
|
|
}
|
|
resp, err := http.Get(urlSearch.String())
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(3 * time.Second)
|
|
return nil, err
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(3 * time.Second)
|
|
return nil, err
|
|
}
|
|
var resultSearch map[string]any
|
|
err = json.Unmarshal(body, &resultSearch)
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(3 * time.Second)
|
|
return nil, err
|
|
}
|
|
apps, ok := resultSearch["apps"].([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("failed to get apps")
|
|
}
|
|
appsList := make([]App, len(apps))
|
|
for i, app := range apps {
|
|
app, ok := app.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("failed to get app")
|
|
}
|
|
appsList[i] = App{
|
|
BundleID: app["bundleID"].(string),
|
|
Version: app["version"].(string),
|
|
}
|
|
}
|
|
return appsList, nil
|
|
}
|
|
|
|
type WriteCounter struct {
|
|
Total int64
|
|
Downloaded int64
|
|
}
|
|
|
|
func (wc *WriteCounter) Write(p []byte) (int, error) {
|
|
n := len(p)
|
|
wc.Downloaded += int64(n)
|
|
wc.PrintProgress()
|
|
return n, nil
|
|
}
|
|
|
|
func (wc *WriteCounter) PrintProgress() {
|
|
const MB = 1024 * 1024
|
|
downloadedMB := float64(wc.Downloaded) / MB
|
|
if wc.Total <= 0 {
|
|
fmt.Printf("\rDownloading... %.2f MB", downloadedMB)
|
|
return
|
|
}
|
|
totalMB := float64(wc.Total) / MB
|
|
percentage := float64(wc.Downloaded) / float64(wc.Total) * 100
|
|
fmt.Printf("\rDownloading... %.2f%% (%.2f/%.2f MB)", percentage, downloadedMB, totalMB)
|
|
}
|
|
|
|
func download(host string, port int, bundleID, query, version, udid, outputPath string) bool {
|
|
linkDownload := fmt.Sprintf(
|
|
"http://%s:%d/download?bundleID=%s", host, port, bundleID,
|
|
)
|
|
|
|
urlDownload, err := url.Parse(linkDownload)
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(3 * time.Second)
|
|
return false
|
|
}
|
|
resp, err := http.Get(urlDownload.String())
|
|
if err != nil {
|
|
fmt.Println("error", err)
|
|
time.Sleep(3 * time.Second)
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
fmt.Printf("error: server returned status %s\n", resp.Status)
|
|
return false
|
|
}
|
|
|
|
fileName := filepath.Join(outputPath, fmt.Sprintf("%s_%s_%s.ipa", udid, query, version))
|
|
out, err := os.Create(fileName)
|
|
if err != nil {
|
|
fmt.Println("error creating file:", err)
|
|
return false
|
|
}
|
|
defer out.Close()
|
|
|
|
counter := &WriteCounter{Total: resp.ContentLength}
|
|
_, err = io.Copy(out, io.TeeReader(resp.Body, counter))
|
|
if err != nil {
|
|
fmt.Println("\nerror downloading:", err)
|
|
return false
|
|
}
|
|
|
|
fmt.Print("\n") // New line after download completion
|
|
return true
|
|
}
|
|
|
|
func checkVersionInDownloaded(udid, query, version, outputPath string) bool {
|
|
path := filepath.Join(outputPath, fmt.Sprintf("%s_%s_%s.ipa", udid, query, version))
|
|
_, err := os.Stat(path)
|
|
return !os.IsNotExist(err)
|
|
}
|
|
|
|
func main() {
|
|
|
|
bundleID := flag.String("b", "com.zhiliaoapp.musically", "Bundle ID of the app to download from IPAGo server")
|
|
host := flag.String("h", "", "Host IPAGo server")
|
|
port := flag.Int("p", 0, "Port IPAGo server")
|
|
udid := flag.String("u", "", "UDID of the device to download the app to")
|
|
user := flag.String("user", "", "User to login to IPAGo server")
|
|
pswd := flag.String("pswd", "", "Password to login to IPAGo server")
|
|
query := flag.String("q", "", "Query to search for apps on IPAGo server (example: TikTok)")
|
|
limit := flag.Int("l", 1, "Limit of apps to search for on IPAGo server")
|
|
outputPath := flag.String("o", "", "Output path to save the app")
|
|
flag.Parse()
|
|
|
|
if *outputPath == "" {
|
|
fmt.Println("output path is required")
|
|
flag.PrintDefaults()
|
|
os.Exit(1)
|
|
}
|
|
|
|
os.MkdirAll(*outputPath, 0755)
|
|
|
|
if *host == "" {
|
|
fmt.Println("host is required")
|
|
flag.PrintDefaults()
|
|
os.Exit(1)
|
|
}
|
|
if *port == 0 {
|
|
fmt.Println("port is required")
|
|
flag.PrintDefaults()
|
|
os.Exit(1)
|
|
}
|
|
if *udid == "" {
|
|
fmt.Println("UDID is required")
|
|
flag.PrintDefaults()
|
|
os.Exit(1)
|
|
}
|
|
if *user == "" {
|
|
fmt.Println("user is required")
|
|
flag.PrintDefaults()
|
|
os.Exit(1)
|
|
}
|
|
if *pswd == "" {
|
|
fmt.Println("password is required")
|
|
flag.PrintDefaults()
|
|
os.Exit(1)
|
|
}
|
|
if *query == "" {
|
|
fmt.Println("query is required")
|
|
flag.PrintDefaults()
|
|
os.Exit(1)
|
|
}
|
|
userQuery := url.QueryEscape(*user)
|
|
pswdQuery := url.QueryEscape(*pswd)
|
|
|
|
for {
|
|
if !authInfo(*host, *port, userQuery, pswdQuery) {
|
|
continue
|
|
}
|
|
|
|
if !authLogin(*host, *port, userQuery, pswdQuery) {
|
|
continue
|
|
}
|
|
|
|
searchResult, err := search(*host, *port, *query, *limit)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// TODO: handle multiple apps with the same bundleID
|
|
srFirst := searchResult[0]
|
|
if srFirst.BundleID != *bundleID {
|
|
fmt.Println("bundleID mismatch, expected", *bundleID, "got", srFirst.BundleID)
|
|
time.Sleep(120 * time.Minute)
|
|
continue
|
|
}
|
|
|
|
if checkVersionInDownloaded(*udid, *query, srFirst.Version, *outputPath) {
|
|
fmt.Println("version already downloaded, skipping...")
|
|
time.Sleep(120 * time.Minute)
|
|
continue
|
|
}
|
|
|
|
if !download(*host, *port, srFirst.BundleID, *query, srFirst.Version, *udid, *outputPath) {
|
|
continue
|
|
}
|
|
|
|
}
|
|
}
|