gamebanana: add function for downloading mods
kinda janky with the implementation right now, later i would like to improve the way files are copied and allow for mods with multiple downloads attached to the mod. probably split off some of the functionality later to add installing mods not on gamebanana easier.
This commit is contained in:
parent
226c6a0e93
commit
527994abba
5 changed files with 188 additions and 19 deletions
143
gamebanana/download.go
Normal file
143
gamebanana/download.go
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
package gamebanana
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/gen2brain/go-unarr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dlurl string
|
||||||
|
|
||||||
|
func DownloadGameBananaMod(id, outdir string) (error) {
|
||||||
|
modData, err := GetGameBananaMod(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create temporary file to download mod to
|
||||||
|
tmpFolderDownload, err := os.MkdirTemp("", "diva-mod-download-*")
|
||||||
|
log.Info("created temp folder for downloading", "name", tmpFolderDownload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// really really jank, but api doesn't really have a better way of doing this
|
||||||
|
for _, x := range modData.FilesAFiles {
|
||||||
|
dlurl = x.SDownloadURL
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
fileDlPath := path.Join(tmpFolderDownload, "download")
|
||||||
|
fileDl, err := os.Create(fileDlPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fileDl.Close()
|
||||||
|
|
||||||
|
// download mod
|
||||||
|
resp, err := http.Get(dlurl)
|
||||||
|
log.Info("downloading mod", "url", dlurl, "modid", id, "path", fileDl)
|
||||||
|
// TODO: handle error code properly
|
||||||
|
if err != nil || resp.StatusCode != 200 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// save to temp file
|
||||||
|
_, err = io.Copy(fileDl, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract files to same directory
|
||||||
|
a, err := unarr.NewArchive(fileDlPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = a.Extract(tmpFolderDownload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// such jank but ive spent wayyy too long on this
|
||||||
|
var moddir string = ""
|
||||||
|
err = filepath.Walk(tmpFolderDownload, func(filePath string, _ fs.FileInfo, err error) error {
|
||||||
|
if path.Base(filePath) == "config.toml" {
|
||||||
|
moddir = path.Dir(filePath)
|
||||||
|
log.Info("found mod folder", "configloc", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if moddir == "" {
|
||||||
|
errors.New("config.toml not found in mod dir")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a folder put the mod in
|
||||||
|
outpath := path.Join(outdir, fmt.Sprintf("%s@%d", id, modData.Udate))
|
||||||
|
err = os.Mkdir(outpath, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = filepath.Walk(moddir, func(filePath string, fileinfo fs.FileInfo, err error) error {
|
||||||
|
relativepath := strings.Replace(filePath, moddir, "", 1)
|
||||||
|
newpath := path.Join(outdir, relativepath)
|
||||||
|
|
||||||
|
if relativepath == "" { return nil }
|
||||||
|
|
||||||
|
if fileinfo.IsDir() == true {
|
||||||
|
log.Warn("found directory, creating it!", "path", filePath, "relpath", relativepath, "newpath", newpath)
|
||||||
|
err = os.Mkdir(newpath, 0744)
|
||||||
|
if err != nil { return err }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var src *os.File
|
||||||
|
var dst *os.File
|
||||||
|
|
||||||
|
if src, err = os.Open(filePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
if dst, err = os.Create(newpath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(dst, src); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("copied file!!!", "src", filePath, "dst", newpath, "relativepath", relativepath)
|
||||||
|
return os.Chmod(newpath, 0744)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: cleanup temp files
|
||||||
|
err = os.RemoveAll(fileDlPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
24
gamebanana/download_test.go
Normal file
24
gamebanana/download_test.go
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
package gamebanana
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDownloadGameBananaMod(t *testing.T) {
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
err := DownloadGameBananaMod("602180", tmpdir)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to download game banana mod", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// udate is the time the mod was last updated
|
||||||
|
// in unix time, used here as a version number
|
||||||
|
// modID@udate
|
||||||
|
expectedPathToExist := path.Join(tmpdir, "602180@1750696716")
|
||||||
|
|
||||||
|
if _, err := os.Stat(expectedPathToExist); err != nil {
|
||||||
|
t.Error("mod doesn't have expected name or doesn't exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,24 +29,7 @@ type GameBananaMod struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Downloads int `json:"downloads"`
|
Downloads int `json:"downloads"`
|
||||||
FeedbackInstructions string `json:"feedback_instructions"`
|
FeedbackInstructions string `json:"feedback_instructions"`
|
||||||
FilesAFiles struct {
|
FilesAFiles map[string]gamebananaDownloadInformation `json:"Files().aFiles()"`
|
||||||
Num884808 struct {
|
|
||||||
IDRow string `json:"_idRow"`
|
|
||||||
SFile string `json:"_sFile"`
|
|
||||||
NFilesize int `json:"_nFilesize"`
|
|
||||||
TsDateAdded int `json:"_tsDateAdded"`
|
|
||||||
NDownloadCount int `json:"_nDownloadCount"`
|
|
||||||
SDownloadURL string `json:"_sDownloadUrl"`
|
|
||||||
SMd5Checksum string `json:"_sMd5Checksum"`
|
|
||||||
SAnalysisState string `json:"_sAnalysisState"`
|
|
||||||
SAnalysisResult string `json:"_sAnalysisResult"`
|
|
||||||
SAnalysisResultVerbose string `json:"_sAnalysisResultVerbose"`
|
|
||||||
SAvState string `json:"_sAvState"`
|
|
||||||
SAvResult string `json:"_sAvResult"`
|
|
||||||
BIsArchived bool `json:"_bIsArchived"`
|
|
||||||
BHasContents bool `json:"_bHasContents"`
|
|
||||||
} `json:"884808"`
|
|
||||||
} `json:"Files().aFiles()"`
|
|
||||||
GameName string `json:"Game().name"`
|
GameName string `json:"Game().name"`
|
||||||
InstallInstructions string `json:"install_instructions"`
|
InstallInstructions string `json:"install_instructions"`
|
||||||
IsObsolete string `json:"is_obsolete"`
|
IsObsolete string `json:"is_obsolete"`
|
||||||
|
|
@ -91,6 +74,22 @@ type GameBananaMod struct {
|
||||||
WithholdBIsWithheld bool `json:"Withhold().bIsWithheld()"`
|
WithholdBIsWithheld bool `json:"Withhold().bIsWithheld()"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type gamebananaDownloadInformation struct {
|
||||||
|
IDRow string `json:"_idRow"`
|
||||||
|
SFile string `json:"_sFile"`
|
||||||
|
NFilesize int `json:"_nFilesize"`
|
||||||
|
TsDateAdded int `json:"_tsDateAdded"`
|
||||||
|
NDownloadCount int `json:"_nDownloadCount"`
|
||||||
|
SDownloadURL string `json:"_sDownloadUrl"`
|
||||||
|
SMd5Checksum string `json:"_sMd5Checksum"`
|
||||||
|
SAnalysisState string `json:"_sAnalysisState"`
|
||||||
|
SAnalysisResult string `json:"_sAnalysisResult"`
|
||||||
|
SAnalysisResultVerbose string `json:"_sAnalysisResultVerbose"`
|
||||||
|
SAvState string `json:"_sAvState"`
|
||||||
|
SAvResult string `json:"_sAvResult"`
|
||||||
|
BIsArchived bool `json:"_bIsArchived"`
|
||||||
|
BHasContents bool `json:"_bHasContents"`
|
||||||
|
}
|
||||||
|
|
||||||
func GetGameBananaMod(id string) (GameBananaMod, error) {
|
func GetGameBananaMod(id string) (GameBananaMod, error) {
|
||||||
// build URL params
|
// build URL params
|
||||||
|
|
@ -105,7 +104,7 @@ func GetGameBananaMod(id string) (GameBananaMod, error) {
|
||||||
params.Add("itemid", id)
|
params.Add("itemid", id)
|
||||||
params.Add("format", "json_min")
|
params.Add("format", "json_min")
|
||||||
params.Add("return_keys", "1")
|
params.Add("return_keys", "1")
|
||||||
params.Add("fields", "apps_used,authors,Category().name,catid,contestid,creator,Credits().aAuthors(),Credits().aAuthorsAndGroups(),Credits().ssvAuthorNames(),date,description,downloads,feedback_instructions,Files().aFiles(),Game().name,install_instructions,is_obsolete,lastpost_date,lastpost_userid,likes,mdate,modnote,name,Nsfw().bIsNsfw(),obsol_notice,Owner().name,postcount,Posts().LastPost().idPosterRow(),Posts().LastPost().sText(),Posts().LastPost().tsDateAdded(),Posts().Postcount().nPostCount(),Preview().sStructuredDataFullsizeUrl(),Preview().sSubFeedImageUrl(),RootCategory().id,RootCategory().name,screenshots,studioid,text,Trash().bIsTrashed(),udate,Updates().aGetLatestUpdates(),Updates().aLatestUpdates(),Updates().bSubmissionHasUpdates(),Updates().nUpdatesCount(),Url().sDownloadUrl(),Url().sEditUrl(),Url().sEmbeddablesUrl(),Url().sHistoryUrl(),Url().sProfileUrl(),Url().sTrashUrl(),Url().sUntrashUrl(),Url().sUpdatesUrl(),Url().sWithholdUrl(),userid,views,Withhold().bIsWithheld()")
|
params.Add("fields", "apps_used,authors,Category().name,catid,contestid,creator,Credits().ssvAuthorNames(),date,description,downloads,feedback_instructions,Files().aFiles(),Game().name,install_instructions,is_obsolete,lastpost_date,lastpost_userid,likes,mdate,modnote,name,Nsfw().bIsNsfw(),obsol_notice,Owner().name,postcount,Posts().LastPost().idPosterRow(),Posts().LastPost().sText(),Posts().LastPost().tsDateAdded(),Posts().Postcount().nPostCount(),Preview().sStructuredDataFullsizeUrl(),Preview().sSubFeedImageUrl(),RootCategory().id,RootCategory().name,screenshots,studioid,text,Trash().bIsTrashed(),udate,Updates().aGetLatestUpdates(),Updates().aLatestUpdates(),Updates().bSubmissionHasUpdates(),Updates().nUpdatesCount(),Url().sDownloadUrl(),Url().sEditUrl(),Url().sEmbeddablesUrl(),Url().sHistoryUrl(),Url().sProfileUrl(),Url().sTrashUrl(),Url().sUntrashUrl(),Url().sUpdatesUrl(),Url().sWithholdUrl(),userid,views,Withhold().bIsWithheld()")
|
||||||
base.RawQuery = params.Encode()
|
base.RawQuery = params.Encode()
|
||||||
|
|
||||||
// send request
|
// send request
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -4,6 +4,7 @@ go 1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
|
github.com/gen2brain/go-unarr v0.2.4
|
||||||
github.com/google/go-cmp v0.5.8
|
github.com/google/go-cmp v0.5.8
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -14,6 +14,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gen2brain/go-unarr v0.2.4 h1:Iu2kqtGfkLBSQoTFwMkSCmp0g3GrEM/XMVWzo9TQr/Y=
|
||||||
|
github.com/gen2brain/go-unarr v0.2.4/go.mod h1:0kdy3HtjKBcEaewifXZguHCvt4qD9V8iJCx4FPEOWT8=
|
||||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue