// Base code taken from: https://gist.github.com/marians/3b55318106df0e4e648158f1ffb43d38 package main import ( "context" "crypto/tls" // "fmt" "log" "net/http" "time" "encoding/json" "github.com/fatih/color" "github.com/webview/webview" "golang.org/x/oauth2" "io/ioutil" "strconv" "fmt" "regexp" "strings" "os" ) var ( conf *oauth2.Config ctx context.Context w webview.WebView client *http.Client ) func saveToken(tok *oauth2.Token, filename string) { j, err := json.MarshalIndent(tok, "", "\t") if err != nil { log.Fatalf("Failed to marshal token: %v", err) } err = ioutil.WriteFile(filename, j, 0600) if err != nil { log.Fatalf("Failed to save token: %v", err) } } func loadToken(filename string) *oauth2.Token { data, err := ioutil.ReadFile(filename) if err != nil { return nil } var v = new(oauth2.Token) err = json.Unmarshal(data, &v) if err != nil { log.Fatalf("Failed to unmarshal data: %v", err) } return v } func giveCode(code string) { log.Printf("Code: %s", code) // Exchange will do the handshake to retrieve the initial access token. tok, err := conf.Exchange(ctx, code) if err != nil { log.Fatal(err) } log.Printf("Token: %s", tok) saveToken(tok, "./.token") // The HTTP Client returned by conf.Client will refresh the token as necessary. client = conf.Client(ctx, tok) _, err = client.Get("https://embed.gog.com/user/data/games") if err != nil { log.Fatal(err) } else { log.Println(color.CyanString("Authentication successful")) } w.Navigate(`data:text/html,

Success!

You are authenticated and can now return to the CLI.

`) time.Sleep(1 * time.Second) w.Terminate() } func openBrowser(url string) { w = webview.New(true) defer w.Destroy() w.SetTitle("Login") w.SetSize(600,600, webview.HintNone) w.Navigate(url) w.Bind("giveCode", giveCode) w.Init(` var params = new URLSearchParams(window.location.search); if (params.has('code')) { giveCode(params.get('code')); } `) w.Run() } type GameList struct { Owned []int `json:"owned"` } func getGameList() *GameList { res, err := client.Get("https://embed.gog.com/user/data/games") if err != nil { log.Fatalf("Failed to get game list: %v", err) } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { log.Fatalf("Failed to parse game body: %v", err) } // log.Println(string(body)) var gl = new(GameList) err = json.Unmarshal(body, gl) if err != nil { log.Fatalf("Failed to parse game list response: %v", err) } return gl } type Product struct { Id int `json:"id"` Title string `json:"title"` Downloads struct { Installers [] struct { Name string `json:"name"` Os string `json:"os"` Language string `json:"language"` Files [] struct { Id string `json:"id"` Size int64 `json:"size"` Downlink string `json:"downlink"` } `json:"files"` } `json:"installers"` } `json:"downloads` } func getProduct(id int) *Product { url := "https://api.gog.com/products/" + strconv.Itoa(id) + "?expand=downloads" res, err := client.Get(url) if err != nil { log.Fatalf("Failed to get product info: %v", err) } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { log.Fatalf("Failed to parse product body: %v", err) } //log.Println(string(body)) var p = new(Product) err = json.Unmarshal(body, p) if err != nil { log.Fatalf("Failed to parse product response: %v", err) } return p } type DownloadLinks struct { Downlink string `json:"downlink"` Checksum string `json:"checksum"` InstallerName string Filename string Md5 string Os string Language string } func (dl *DownloadLinks) String() string { return fmt.Sprintf("{downlink: %s, checksum: %s}", dl.Downlink, dl.Checksum) } func getDownloadLinks(p *Product) []*DownloadLinks { var links []*DownloadLinks for _, i := range p.Downloads.Installers { for _, f := range i.Files { res, err := client.Get(f.Downlink) if err != nil { log.Fatalf("Failed to get file download info: %v", err) } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { log.Fatalf("Failed to parse file download body: %v", err) } //log.Println(string(body)) var dl = new(DownloadLinks) err = json.Unmarshal(body, dl) if err != nil { log.Fatalf("Failed to parse file download response: %v", err) } dl.InstallerName = sanitizeName(i.Name) dl.Os = i.Os dl.Language = i.Language links = append(links, dl) } } return links } func sanitizeName(name string) string { // s := strings.Replace(name, " ", "_", -1) // s = strings.Replace(s, "'", "", -1) // s = strings.Replace(s, "™", "", -1) re := regexp.MustCompile("[^0-9a-zA-Z_.-]+") s := name s = re.ReplaceAllString(s, "") return s } func updateBashScriptProductFolder(p *Product, folder string, scriptName string, productNumber int, totalProducts int) { f, err := os.OpenFile("./" + scriptName, os.O_APPEND|os.O_WRONLY, 0664) if err != nil { log.Fatalf("Failed to open the script file: %v", err) } defer f.Close() // Get a list of the OS's var osList []string for _, i := range p.Downloads.Installers { exists := false for _, o := range osList { if o == sanitizeName(i.Os) + "/" + sanitizeName(i.Language) { exists = true break } } if !exists { osList = append(osList, sanitizeName(i.Os) + "/" + sanitizeName(i.Language)) } } // Print out the product _, err = f.WriteString("echo \"[" + strconv.Itoa(productNumber) + " of " + strconv.Itoa(totalProducts) + "]Downloading files for: " + sanitizeName(p.Title) + "\"\n") if err != nil { log.Fatalf("Failed to update the script: %v", err) } // Create a folder for each os installers for _, o := range osList { _, err = f.WriteString("mkdir -p " + folder + "/" + sanitizeName(p.Title) + "/" + o + "\n") if err != nil { log.Fatalf("Failed to update the script: %v", err) } } } func writeBashScript(dl *DownloadLinks, folder string, scriptName string, productNumber int, totalProducts int, fileNumber int, totalFiles int) { // Find out file name and size; have to follow redirects res, err := client.Head(dl.Downlink) if err != nil { log.Fatalf("Failed to do a head request for the download file: %v", err) } defer res.Body.Close() // log.Printf("head request url: %+v", res.Request.URL.Path) pp := strings.Split(res.Request.URL.Path, "/") dl.Filename = sanitizeName(pp[len(pp) - 1]) // Add the lines to the script line := "echo \"[" + strconv.Itoa(productNumber) + " of " + strconv.Itoa(totalProducts) + "]{" + strconv.Itoa(fileNumber) + "/" + strconv.Itoa(totalFiles) + "}Getting installers for (" + dl.InstallerName + ")" + "\"\n" line += "wget --no-clobber --continue --quiet --show-progress -O \"" + folder + "/" + sanitizeName(dl.InstallerName) + "/" + sanitizeName(dl.Os) + "/" + sanitizeName(dl.Language) + "/" + dl.Filename + ".xml\" \"" + dl.Checksum + "\"\n" line += "wget --no-clobber --continue --quiet --show-progress -O \"" + folder + "/" + sanitizeName(dl.InstallerName) + "/" + sanitizeName(dl.Os) + "/" + sanitizeName(dl.Language) + "/" + dl.Filename + "\" \"" + dl.Downlink + "\"\n" f, err := os.OpenFile("./" + scriptName, os.O_APPEND|os.O_WRONLY, 0664) if err != nil { log.Fatalf("Failed to open the script file: %v", err) } defer f.Close() _, err = f.WriteString(line) if err != nil { log.Fatalf("Failed to update the script: %v", err) } } func login(url string) { log.Println(color.CyanString("You will now be taken to your browser for authentication")) time.Sleep(1 * time.Second) openBrowser(url) } func main() { productsFolder := "./products" scriptName := "grab_stuff.sh" ctx = context.Background() conf = &oauth2.Config{ ClientID: "46899977096215655", ClientSecret: "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9", Scopes: []string{}, Endpoint: oauth2.Endpoint{ AuthURL: "https://auth.gog.com/auth", TokenURL: "https://auth.gog.com/token", }, // my own callback URL RedirectURL: "https://embed.gog.com/on_login_success?origin=client", } // add transport tr := &http.Transport{ TLSClientConfig: &tls.Config{}, } sslcli := &http.Client{Transport: tr} ctx = context.WithValue(ctx, oauth2.HTTPClient, sslcli) // Redirect user to consent page to ask for permission // for the scopes specified above. url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline) tok := loadToken("./.token") if tok != nil { client = conf.Client(ctx, tok) _, err := client.Get("https://embed.gog.com/user/data/games") if err != nil { login(url) } else { log.Println(color.CyanString("Authentication successful")) } } else { login(url) } gl := getGameList() log.Println(gl.Owned) ioutil.WriteFile("./" + scriptName, []byte("#!/bin/bash\n"), 0775) for index, id := range gl.Owned { log.Printf("Inspecting product: %d of %d - %d", index, len(gl.Owned), id) p := getProduct(id) dl := getDownloadLinks(p) updateBashScriptProductFolder(p, productsFolder, scriptName, index+1, len(gl.Owned)) for idx, d := range dl { //writeChecksumIfNeeded(d, productsFolder + "/" + sanitizeName(p.Title)) writeBashScript(d, productsFolder, scriptName, index+1, len(gl.Owned), idx+1, len(dl)) } } }