244 lines
6.9 KiB
Go
244 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
|
utils "git.site.quack-lab.dev/dave/cyutils"
|
|
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
const (
|
|
steamcmdEnvKey = "STEAMCMD"
|
|
steamcmdScriptPathEnvKey = "STEAMCMD_SCRIPT"
|
|
appEnvKey = "APP"
|
|
usernameEnvKey = "USERNAME"
|
|
passwordEnvKey = "PASSWORD"
|
|
)
|
|
|
|
func main() {
|
|
envfile := flag.String("envfile", ".env", "")
|
|
inputfile := flag.String("if", "", "")
|
|
pocketbase := flag.Bool("pb", false, "")
|
|
batchsize := flag.Int("bs", 10, "")
|
|
workers := flag.Int("w", 30, "")
|
|
flag.Parse()
|
|
logger.InitFlag()
|
|
|
|
pocketbaseids := make([]string, 0)
|
|
if *pocketbase {
|
|
ids, err := FromPocketbase(pocketbaseUrl, "445220")
|
|
if err != nil {
|
|
logger.Error("Error getting workshop ids: %v", err)
|
|
return
|
|
}
|
|
logger.Info("Got %d workshop ids", len(ids))
|
|
pocketbaseids = append(pocketbaseids, ids...)
|
|
}
|
|
|
|
args := flag.Args()
|
|
if *inputfile != "" {
|
|
filehandle, err := os.Open(*inputfile)
|
|
if err != nil {
|
|
logger.Error("Error opening input file: %v", err)
|
|
return
|
|
}
|
|
defer filehandle.Close()
|
|
fargs, err := io.ReadAll(filehandle)
|
|
if err != nil {
|
|
logger.Error("Error reading input file: %v", err)
|
|
return
|
|
}
|
|
sargs := strings.Split(string(fargs), "\r\n")
|
|
args = append(args, sargs...)
|
|
}
|
|
|
|
if len(args) == 0 && len(pocketbaseids) == 0 {
|
|
logger.Error("No args specified, please pass space delimited list of workshop ids for downloading or use -pb to get ids from pocketbase")
|
|
return
|
|
}
|
|
toDownload := make([]string, 0, len(args)+len(pocketbaseids))
|
|
toDownload = append(toDownload, args...)
|
|
toDownload = append(toDownload, pocketbaseids...)
|
|
|
|
env, err := loadEnv(*envfile)
|
|
if err != nil {
|
|
logger.Error("Error leading env file: %v", err)
|
|
return
|
|
}
|
|
|
|
steamcmdPath, ok := env[steamcmdEnvKey]
|
|
if !ok {
|
|
logger.Error("SteamCMD not found in env, please specify '%s'", steamcmdEnvKey)
|
|
return
|
|
}
|
|
steamcmdPath = filepath.Clean(steamcmdPath)
|
|
steamcmdExe := filepath.Join(steamcmdPath, "steamcmd.exe")
|
|
tempfile, err := os.Open(steamcmdExe)
|
|
if err != nil {
|
|
logger.Error("Error opening SteamCMD, does it exist?: %v", err)
|
|
return
|
|
}
|
|
tempfile.Close()
|
|
|
|
batches := make([][]string, 0, len(toDownload)/(*batchsize))
|
|
utils.Batched(toDownload, *batchsize, func(batch []string) {
|
|
batches = append(batches, batch)
|
|
})
|
|
|
|
app, ok := env[appEnvKey]
|
|
if !ok {
|
|
logger.Error("App not found in env, please specify '%s'", appEnvKey)
|
|
return
|
|
}
|
|
|
|
username, ok := env[usernameEnvKey]
|
|
if !ok {
|
|
username = "anonymous"
|
|
logger.Info("Username not found in env, using '%s'", username)
|
|
logger.Info("If you want to log in specify '%s' and '%s'", usernameEnvKey, passwordEnvKey)
|
|
}
|
|
password, ok := env[passwordEnvKey]
|
|
if !ok {
|
|
password = ""
|
|
logger.Info("Password not found in env, using empty")
|
|
}
|
|
|
|
utils.WithWorkers(*workers, batches, func(worker int, batch []string) {
|
|
steamcmdScriptPath, ok := env[steamcmdScriptPathEnvKey]
|
|
if !ok {
|
|
steamcmdScriptPath = filepath.Join(steamcmdPath, fmt.Sprintf("script-%d.txt", worker))
|
|
logger.Info("SteamCMD script not found in env, using '%s'", steamcmdScriptPath)
|
|
logger.Info("Specify script path with '%s'", steamcmdScriptPathEnvKey)
|
|
}
|
|
|
|
logger.Info("Using steamcmd at '%s'", steamcmdPath)
|
|
logger.Info("As user '%s'", username)
|
|
logger.Info("Downloading %d items for '%s'", len(batch), app)
|
|
|
|
scriptContents := make([]string, 0, len(args)+4)
|
|
scriptContents = append(scriptContents, "@ShutdownOnFailedCommand 0")
|
|
scriptContents = append(scriptContents, "@NoPromptForPassword 1")
|
|
scriptContents = append(scriptContents, fmt.Sprintf("login %s %s", username, password))
|
|
for _, id := range batch {
|
|
scriptContents = append(scriptContents, fmt.Sprintf("workshop_download_item %s %s", app, id))
|
|
}
|
|
scriptContents = append(scriptContents, "quit")
|
|
|
|
scriptFileHandle, err := os.OpenFile(steamcmdScriptPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755)
|
|
if err != nil {
|
|
logger.Error("Could not open steamcmd script file")
|
|
return
|
|
}
|
|
defer scriptFileHandle.Close()
|
|
_, err = scriptFileHandle.Write([]byte(strings.Join(scriptContents, "\n")))
|
|
if err != nil {
|
|
logger.Error("Could not write to steamcmd script file")
|
|
return
|
|
}
|
|
logger.Info("Wrote %d lines to script file at '%s'", len(scriptContents), steamcmdScriptPath)
|
|
|
|
steamcmdExe, _ = filepath.Abs(steamcmdExe)
|
|
steamcmdScriptPath, _ = filepath.Abs(steamcmdScriptPath)
|
|
logger.Info("Running steamcmd at %s", steamcmdExe)
|
|
cmd := exec.Command(steamcmdExe, "+runscript", steamcmdScriptPath)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Dir = steamcmdPath
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
logger.Error("SteamCMD failed with code %v", err)
|
|
return
|
|
}
|
|
logger.Info("Batch %d finished", worker)
|
|
})
|
|
|
|
logger.Info("All done")
|
|
}
|
|
|
|
func loadEnv(fpath string) (map[string]string, error) {
|
|
res := make(map[string]string)
|
|
|
|
fpath = filepath.Clean(fpath)
|
|
logger.Info("Trying to open env file at '%s'", fpath)
|
|
|
|
file, err := os.Open(fpath)
|
|
if err != nil {
|
|
return res, fmt.Errorf("error opening env file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
res, err = godotenv.Parse(file)
|
|
if err != nil {
|
|
return res, fmt.Errorf("error parsing env file: %w", err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
type PocketbaseResponse struct {
|
|
Page int64 `json:"page"`
|
|
PerPage int64 `json:"perPage"`
|
|
TotalPages int64 `json:"totalPages"`
|
|
TotalItems int64 `json:"totalItems"`
|
|
Items []WorkshopItem `json:"items"`
|
|
}
|
|
|
|
type WorkshopItem struct {
|
|
CollectionID string `json:"collectionId"`
|
|
CollectionName string `json:"collectionName"`
|
|
ID string `json:"id"`
|
|
Gameid string `json:"gameid"`
|
|
Workshopid string `json:"workshopid"`
|
|
Title string `json:"title"`
|
|
Link string `json:"link"`
|
|
LastUpdated string `json:"lastUpdated"`
|
|
Subscribed string `json:"subscribed"`
|
|
Created string `json:"created"`
|
|
Updated string `json:"updated"`
|
|
}
|
|
|
|
var pocketbaseUrl = "https://pocketbase-scratch.site.quack-lab.dev/api/collections/steam_workshop/records"
|
|
|
|
func FromPocketbase(url string, appId string) ([]string, error) {
|
|
url = fmt.Sprintf("%s?filter=(gameid='%s')&fields=workshopid&perPage=1000", pocketbaseUrl, appId)
|
|
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var res PocketbaseResponse
|
|
err = json.Unmarshal(body, &res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Deduplicate
|
|
ids := make(map[string]struct{})
|
|
for _, entry := range res.Items {
|
|
ids[entry.Workshopid] = struct{}{}
|
|
}
|
|
idsarr := make([]string, 0, len(ids))
|
|
for id := range ids {
|
|
idsarr = append(idsarr, id)
|
|
}
|
|
|
|
return idsarr, nil
|
|
}
|