HEAD request on the installer and uses the file name on the url path after redirects to name it. Also adds downloading the xml checksum.
361 lines
8.7 KiB
Go
361 lines
8.7 KiB
Go
// 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,
|
|
<!doctype html>
|
|
<html>
|
|
<body>
|
|
<p><strong>Success!</strong></p>
|
|
<p>You are authenticated and can now return to the CLI.</p>
|
|
</body>
|
|
</html>
|
|
`)
|
|
|
|
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
|
|
}
|
|
|
|
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)
|
|
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-1a-zA-Z.-_]+")
|
|
s := name
|
|
s = re.ReplaceAllString(s, "_")
|
|
return s
|
|
}
|
|
|
|
// func writeChecksumIfNeeded(dl *DownloadLinks, folder string) {
|
|
// // TODO: reuse an existing checksum file somehow
|
|
// res, err := client.Get(dl.Checksum)
|
|
// if err != nil {
|
|
// log.Fatalf("Failed to get file checksum info (%s): %v", dl.Checksum, err)
|
|
// }
|
|
// defer res.Body.Close()
|
|
// body, err := ioutil.ReadAll(res.Body)
|
|
// if err != nil {
|
|
// log.Fatalf("Failed to parse file checksum body (%s): %v", dl.Checksum, err)
|
|
// }
|
|
|
|
// re := regexp.MustCompile(`<file name="([^"]+)".*md5="([^"]+)`)
|
|
// m := re.FindSubmatch(body)
|
|
// if m != nil {
|
|
// dl.Filename = sanitizeName(string(m[1]))
|
|
// dl.Md5 = string(m[2])
|
|
|
|
// // err = ioutil.WriteFile(folder + "/" + sanitizeName(dl.InstallerName) + "/" + dl.Filename + ".md5", m[2], 0664)
|
|
// // if err != nil {
|
|
// // log.Fatalf("Failed to write the checksum file: %v", err)
|
|
// // }
|
|
// }
|
|
// }
|
|
|
|
|
|
func updateBashScriptProductFolder(p *Product, folder string, scriptName string) {
|
|
|
|
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("mkdir -p " + folder + "/" + sanitizeName(p.Title) + "\n")
|
|
if err != nil {
|
|
log.Fatalf("Failed to update the script: %v", err)
|
|
}
|
|
}
|
|
|
|
|
|
func writeBashScript(dl *DownloadLinks, folder string, scriptName string) {
|
|
// 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 := "wget --no-clobber --continue --quiet --show-progress -O \"" + folder + "/" + sanitizeName(dl.InstallerName) + "/" + dl.Filename + ".xml\" \"" + dl.Checksum + "\"\n"
|
|
line += "wget --no-clobber --continue --quiet --show-progress -O \"" + folder + "/" + sanitizeName(dl.InstallerName) + "/" + 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)
|
|
|
|
for _, d := range dl {
|
|
//writeChecksumIfNeeded(d, productsFolder + "/" + sanitizeName(p.Title))
|
|
writeBashScript(d, productsFolder, scriptName)
|
|
}
|
|
}
|
|
|
|
}
|
|
|