gogdl/main.go
Kevin Ruffin f631a23ae8 Fixes the md5 check so that it only runs if it was able to download the
checksum file and parse it. Also changes a failing checksum to move the
file to the same name with a .bk extension.
2022-03-06 21:17:29 -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 += "[ -f \"" + file + ".md5\" ] && ! md5sum -c \"" + file + ".md5\" && mv \"" + file + "\" \"" + file + ".bk\" && 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))
}
}
}