package main import ( "encoding/json" "flag" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "strings" logger "git.site.quack-lab.dev/dave/cylogger" "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, "") 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 } 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() steamcmdScriptPath, ok := env[steamcmdScriptPathEnvKey] if !ok { steamcmdScriptPath = filepath.Join(steamcmdPath, "script") logger.Info("SteamCMD script not found in env, using '%s'", steamcmdScriptPath) logger.Info("Specify script path with '%s'", steamcmdScriptPathEnvKey) } 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") } logger.Info("Using steamcmd at '%s'", steamcmdPath) logger.Info("As user '%s'", username) logger.Info("Downloading %d items for '%s'", len(args), 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 _, arg := range args { scriptContents = append(scriptContents, fmt.Sprintf("workshop_download_item %s %s", app, arg)) } for _, id := range pocketbaseids { 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", len(scriptContents)) steamcmdExe, _ = filepath.Abs(steamcmdExe) steamcmdScriptPath, _ = filepath.Abs(steamcmdScriptPath) logger.Info("Running steamcmd at %s", steamcmdExe) cmd := exec.Command(steamcmdExe, "+runscript", steamcmdScriptPath) cmd.Dir = steamcmdPath cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin err = cmd.Run() if err != nil { logger.Error("SteamCMD failed with code %v", err) return } 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 }