diff --git a/gamebanana/mods.go b/gamebanana/mods.go new file mode 100644 index 0000000..b4ce7da --- /dev/null +++ b/gamebanana/mods.go @@ -0,0 +1,139 @@ +package gamebanana + +import ( + "encoding/json" + "io" + "net/http" + "net/url" + + "github.com/charmbracelet/log" +) + +// all fields for the "mod" type got at GET api.gamebanana.com/Core/Item/Data/AllowedFields?itemtype=Mod +// struct is gotten from requesting all fields and then converting them with https://transform.tools/json-to-go +type GameBananaMod struct { + AppsUsed string `json:"apps_used"` + Authors string `json:"authors"` + CategoryName string `json:"Category().name"` + Catid int `json:"catid"` + Contestid int `json:"contestid"` + Creator string `json:"creator"` + CreditsAAuthors [][]string `json:"Credits().aAuthors()"` + CreditsAAuthorsAndGroups struct { + KeyAuthor [][]string `json:"Key Author"` + ModelSources [][]string `json:"Model Sources"` + SpecialThanks [][]string `json:"Special Thanks"` + } `json:"Credits().aAuthorsAndGroups()"` + CreditsSsvAuthorNames string `json:"Credits().ssvAuthorNames()"` + Date int `json:"date"` + Description string `json:"description"` + Downloads int `json:"downloads"` + FeedbackInstructions string `json:"feedback_instructions"` + FilesAFiles struct { + 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"` + InstallInstructions string `json:"install_instructions"` + IsObsolete string `json:"is_obsolete"` + LastpostDate int `json:"lastpost_date"` + LastpostUserid int `json:"lastpost_userid"` + Likes int `json:"likes"` + Mdate int `json:"mdate"` + Modnote string `json:"modnote"` + Name string `json:"name"` + NsfwBIsNsfw bool `json:"Nsfw().bIsNsfw()"` + ObsolNotice string `json:"obsol_notice"` + OwnerName string `json:"Owner().name"` + Postcount int `json:"postcount"` + PostsLastPostIDPosterRow int `json:"Posts().LastPost().idPosterRow()"` + PostsLastPostSText string `json:"Posts().LastPost().sText()"` + PostsLastPostTsDateAdded int `json:"Posts().LastPost().tsDateAdded()"` + PostsPostcountNPostCount int `json:"Posts().Postcount().nPostCount()"` + PreviewSStructuredDataFullsizeURL string `json:"Preview().sStructuredDataFullsizeUrl()"` + PreviewSSubFeedImageURL string `json:"Preview().sSubFeedImageUrl()"` + RootCategoryID int `json:"RootCategory().id"` + RootCategoryName string `json:"RootCategory().name"` + Screenshots string `json:"screenshots"` + Studioid int `json:"studioid"` + Text string `json:"text"` + TrashBIsTrashed bool `json:"Trash().bIsTrashed()"` + Udate int `json:"udate"` + UpdatesAGetLatestUpdates []interface{} `json:"Updates().aGetLatestUpdates()"` + UpdatesALatestUpdates []interface{} `json:"Updates().aLatestUpdates()"` + UpdatesBSubmissionHasUpdates bool `json:"Updates().bSubmissionHasUpdates()"` + UpdatesNUpdatesCount int `json:"Updates().nUpdatesCount()"` + URLSDownloadURL string `json:"Url().sDownloadUrl()"` + URLSEditURL string `json:"Url().sEditUrl()"` + URLSEmbeddablesURL string `json:"Url().sEmbeddablesUrl()"` + URLSHistoryURL string `json:"Url().sHistoryUrl()"` + URLSProfileURL string `json:"Url().sProfileUrl()"` + URLSTrashURL string `json:"Url().sTrashUrl()"` + URLSUntrashURL string `json:"Url().sUntrashUrl()"` + URLSUpdatesURL string `json:"Url().sUpdatesUrl()"` + URLSWithholdURL string `json:"Url().sWithholdUrl()"` + Userid int `json:"userid"` + Views int `json:"views"` + WithholdBIsWithheld bool `json:"Withhold().bIsWithheld()"` +} + + +func GetGameBananaMod(id string) (GameBananaMod, error) { + // build URL params + base, err := url.Parse("https://api.gamebanana.com/Core/Item/Data") + if err != nil { + log.Error("failed to parse URL", "err", err) + return GameBananaMod{}, err + } + + params := url.Values{} + params.Add("itemtype", "Mod") + params.Add("itemid", id) + params.Add("format", "json_min") + 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()") + base.RawQuery = params.Encode() + + // send request + log.Info("got request url for Mod information", "id", id, "url", base.String()) + resp, err := http.Get(base.String()) + + if err != nil || resp.StatusCode != 200 { + log.Error("failed to request URL", "url", base.String(), "err", err, "httpstatus", resp.StatusCode) + return GameBananaMod{}, err + } + defer resp.Body.Close() + + + // decode response + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Error("failed to decode body into bytes") + return GameBananaMod{}, err + } + + + var dataStruct GameBananaMod + err = json.Unmarshal(body, &dataStruct) + if err != nil { + log.Error("failed to decode request body", "err", err) + log.Debug(string(body)) + return GameBananaMod{}, err + } + + return dataStruct, nil +} diff --git a/gamebanana/mods_test.go b/gamebanana/mods_test.go new file mode 100644 index 0000000..6e40128 --- /dev/null +++ b/gamebanana/mods_test.go @@ -0,0 +1,26 @@ +package gamebanana + +import ( + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestGetGameBananaModValid(t *testing.T) { + // mod: https://gamebanana.com/mods/409728 + data, err := GetGameBananaMod("409728") + + // data + compdata := `{"apps_used":"","authors":"{\"Key Author\":[[\"2VE\",\"Made the mod\",\"2329267\",\"\"]],\"Model Sources\":[[\" jc-starstorm\",\"Walter White\",\"0\",\"https:\\/\\/www.deviantart.com\\/jc-starstorm\\/art\\/Walter-White-650574338\"],[\" SovietMentality\",\"Jesse Pinkman\",\"0\",\"https:\\/\\/www.deviantart.com\\/sovietmentality\\/art\\/XNA-Breaking-Bad-Jesse-And-Skinny-Pete-DL-483365989\"]],\"Special Thanks\":[[\"Skyth\",\"DML, MMM and DBConverter\",\"1614680\",\"\"],[\"Bubble39\",\"DivaTableManager\",\"0\",\"\"]]}","Category().name":"Modules","catid":17293,"contestid":0,"creator":"true","Credits().aAuthors()":[["2VE","Made the mod","2329267",""],[" jc-starstorm","Walter White","0","https://www.deviantart.com/jc-starstorm/art/Walter-White-650574338"],[" SovietMentality","Jesse Pinkman","0","https://www.deviantart.com/sovietmentality/art/XNA-Breaking-Bad-Jesse-And-Skinny-Pete-DL-483365989"],["Skyth","DML, MMM and DBConverter","1614680",""],["Bubble39","DivaTableManager","0",""]],"Credits().aAuthorsAndGroups()":{"Key Author":[["2VE","Made the mod","2329267",""]],"Model Sources":[[" jc-starstorm","Walter White","0","https://www.deviantart.com/jc-starstorm/art/Walter-White-650574338"],[" SovietMentality","Jesse Pinkman","0","https://www.deviantart.com/sovietmentality/art/XNA-Breaking-Bad-Jesse-And-Skinny-Pete-DL-483365989"]],"Special Thanks":[["Skyth","DML, MMM and DBConverter","1614680",""],["Bubble39","DivaTableManager","0",""]]},"Credits().ssvAuthorNames()":"2VE jc-starstorm SovietMentality Skyth Bubble39","date":1667222980,"description":"Jesse, it's time to dance.","downloads":7335,"feedback_instructions":"","Files().aFiles()":{"884808":{"_idRow":"884808","_sFile":"breaking_bad.rar","_nFilesize":24933477,"_tsDateAdded":1667221651,"_nDownloadCount":7335,"_sDownloadUrl":"https://gamebanana.com/dl/884808","_sMd5Checksum":"0f38122833f1b73c0451c51b4381b301","_sAnalysisState":"done","_sAnalysisResult":"ok","_sAnalysisResultVerbose":"File passed preliminary analysis","_sAvState":"done","_sAvResult":"clean","_bIsArchived":false,"_bHasContents":true}},"Game().name":"Hatsune Miku: Project DIVA Mega Mix+","install_instructions":"","is_obsolete":"false","lastpost_date":1731516780,"lastpost_userid":3011973,"likes":42,"mdate":1667222980,"modnote":"","name":"Breaking Bad","Nsfw().bIsNsfw()":false,"obsol_notice":"","Owner().name":"2VE","postcount":17,"Posts().LastPost().idPosterRow()":3011973,"Posts().LastPost().sText()":"\u003cp\u003eBRBA IN DIVA!1!\u003c/p\u003e","Posts().LastPost().tsDateAdded()":1731516780,"Posts().Postcount().nPostCount()":17,"Preview().sStructuredDataFullsizeUrl()":"https://gamebanana.com/mods/embeddables/409728?variant=sd_image","Preview().sSubFeedImageUrl()":"https://images.gamebanana.com/img/ss/mods/220-90_635fc7a11487a.jpg","RootCategory().id":17315,"RootCategory().name":"Customization","screenshots":"[{\"_sCaption\":\"\",\"_sFile\":\"635fc7a11487a.jpg\",\"_nFilesize\":234709,\"_sFile220\":\"220-90_635fc7a11487a.jpg\",\"_hFile220\":124,\"_wFile220\":220,\"_sFile530\":\"530-90_635fc7a11487a.jpg\",\"_hFile530\":299,\"_wFile530\":530,\"_sFile100\":\"100-90_635fc7a11487a.jpg\",\"_hFile100\":56,\"_wFile100\":100,\"_sFile800\":\"800-90_635fc7a11487a.jpg\",\"_hFile800\":452,\"_wFile800\":800},{\"_sCaption\":\"\",\"_sFile\":\"635fc7d692f95.jpg\",\"_nFilesize\":390948,\"_sFile100\":\"100-90_635fc7d692f95.jpg\",\"_hFile100\":56,\"_wFile100\":100},{\"_sCaption\":\"\",\"_sFile\":\"635fc7e16fd2a.jpg\",\"_nFilesize\":398123,\"_sFile100\":\"100-90_635fc7e16fd2a.jpg\",\"_hFile100\":56,\"_wFile100\":100},{\"_sCaption\":\"\",\"_sFile\":\"635fc7eb51efb.jpg\",\"_nFilesize\":308231,\"_sFile100\":\"100-90_635fc7eb51efb.jpg\",\"_hFile100\":56,\"_wFile100\":100},{\"_sCaption\":\"\",\"_sFile\":\"635fc7f4e96f7.jpg\",\"_nFilesize\":316402,\"_sFile100\":\"100-90_635fc7f4e96f7.jpg\",\"_hFile100\":56,\"_wFile100\":100},{\"_sCaption\":\"\",\"_sFile\":\"635fc7ff0bc69.jpg\",\"_nFilesize\":272481,\"_sFile100\":\"100-90_635fc7ff0bc69.jpg\",\"_hFile100\":56,\"_wFile100\":100},{\"_sCaption\":\"\",\"_sFile\":\"635fc80915a9c.jpg\",\"_nFilesize\":215135,\"_sFile100\":\"100-90_635fc80915a9c.jpg\",\"_hFile100\":56,\"_wFile100\":100},{\"_sCaption\":\"\",\"_sFile\":\"635fc811375e5.jpg\",\"_nFilesize\":271437,\"_sFile100\":\"100-90_635fc811375e5.jpg\",\"_hFile100\":56,\"_wFile100\":100}]","studioid":0,"text":"\u003ci\u003eThis mod ports Walter White and Jesse Pinkman from the hit TV series Breaking Badâ„¢!\u003cbr\u003e\u003c/i\u003e\u003cbr\u003e\u003cbr\u003e\u003ci\u003e\u003cb\u003eMod features\u003c/b\u003e-\u003c/i\u003e\u003cbr\u003e\u003cb\u003eAdded modules:\u003c/b\u003e Both Walter and Jesse are their separate modules with their own portraits, so you can still use official modules without any conflicts.\u003cbr\u003e\u003cb\u003eWorking expressions: \u003c/b\u003eYes, the lips move and will sing along to songs.\u003cbr\u003e\u003cb\u003eFunny:\u003c/b\u003e\u0026nbsp;This mod was initially made as a joke and tested on intel hd in 480p.\u003cbr\u003e\u003cbr\u003e\u003cbr\u003e\u003cb\u003eCompatibility and Known issues-\u003cbr\u003e\u003c/b\u003eWhile this mod doesn't seem to conflict with official assets, the same cannot be said for the other mods on the site as I cannot test or adjust the files to account for mods other than my own.\u003cbr\u003e\u003cbr\u003eAnd as for the issues-\u003cbr\u003eConsidering the nature of the mod (and it being my first for MM+), the character proportions might seem a bit off in some places. This is due to matching the proportions with the official models (or at least their bones), though the mod looks decent otherwise.\u003cbr\u003e\u003cbr\u003e\u003cbr\u003e\u003ci\u003eThanks for reading.\u003c/i\u003e","Trash().bIsTrashed()":false,"udate":1667222980,"Updates().aGetLatestUpdates()":[],"Updates().aLatestUpdates()":[],"Updates().bSubmissionHasUpdates()":false,"Updates().nUpdatesCount()":0,"Url().sDownloadUrl()":"https://gamebanana.com/mods/download/409728","Url().sEditUrl()":"https://gamebanana.com/mods/edit/409728","Url().sEmbeddablesUrl()":"https://gamebanana.com/mods/embeddables/409728","Url().sHistoryUrl()":"https://gamebanana.com/mods/admin/history/409728","Url().sProfileUrl()":"https://gamebanana.com/mods/409728","Url().sTrashUrl()":"https://gamebanana.com/mods/admin/trash/409728","Url().sUntrashUrl()":"https://gamebanana.com/mods/admin/untrash/409728","Url().sUpdatesUrl()":"https://gamebanana.com/mods/updates/409728","Url().sWithholdUrl()":"https://gamebanana.com/mods/admin/withhold/409728","userid":2329267,"views":53290,"Withhold().bIsWithheld()":false}` + + var compDataStruct GameBananaMod + err = json.Unmarshal([]byte(compdata), &compDataStruct) + if err != nil { + t.Fatal("failed to unmarshal test comparison data") + } + + if ! cmp.Equal(compDataStruct, data) { + t.Fatal("data recieved not same as expected data") + } +}