gogdl/main.go
Kevin Ruffin 994883c588 Adds creating a checksum file and verifying the download with it to the
bash script. Right now it removes the file if it doesn't match the
checksum, but should probably do something like rename it or just output
that it doesn't pass.
2022-02-19 22:20:35 -05:00

370 lines
9.4 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
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
path := folder + "/" + sanitizeName(dl.InstallerName) + "/" + sanitizeName(dl.Os) + "/" + sanitizeName(dl.Language) + "/"
file := path + dl.Filename
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 \"" + file + ".xml\" \"" + dl.Checksum + "\"\n"
line += "wget --no-clobber --continue --quiet --show-progress -O \"" + file + "\" \"" + dl.Downlink + "\"\n"
line += "[ ! -f \"" + file + ".md5\" ] && cat \"" + file + ".xml\" | tr '\\n' '\\r' | sed -E 's|<file name=\"([^\"]+)\".*md5=\"([^\"]+).*|\\2 " + file + "\\n|' > " + file + ".md5\n"
line += "! md5sum -c \"" + file + ".md5\" && rm \"" + file + "\" && echo \"Removed unmatched file: " + file + "\"\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))
}
}
}