mirror of
https://github.com/packwiz/packwiz.git
synced 2025-05-05 12:06:31 +02:00
Modrinth Support (#11)
Co-authored-by: comp500 <comp500@users.noreply.github.com>
This commit is contained in:
parent
f2c6b89932
commit
b5b9fd6810
@ -2,16 +2,19 @@ package core
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"strings"
|
||||
"errors"
|
||||
"hash"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetHashImpl gets an implementation of hash.Hash for the given hash type string
|
||||
func GetHashImpl(hashType string) (hash.Hash, error) {
|
||||
switch strings.ToLower(hashType) {
|
||||
case "sha1":
|
||||
return sha1.New(), nil
|
||||
case "sha256":
|
||||
return sha256.New(), nil
|
||||
case "sha512":
|
||||
|
@ -3,12 +3,12 @@ package core
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"fmt"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
1
go.mod
1
go.mod
@ -19,6 +19,7 @@ require (
|
||||
github.com/spf13/cobra v0.0.5
|
||||
github.com/spf13/viper v1.4.0
|
||||
github.com/vbauerster/mpb/v4 v4.7.0
|
||||
golang.org/x/mod v0.4.1
|
||||
golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13
|
||||
gopkg.in/dixonwille/wlog.v2 v2.0.0 // indirect
|
||||
gopkg.in/dixonwille/wmenu.v4 v4.0.2
|
||||
|
9
go.sum
9
go.sum
@ -145,18 +145,24 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -174,6 +180,9 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
|
1
main.go
1
main.go
@ -4,6 +4,7 @@ import (
|
||||
// Modules of packwiz
|
||||
"github.com/comp500/packwiz/cmd"
|
||||
_ "github.com/comp500/packwiz/curseforge"
|
||||
_ "github.com/comp500/packwiz/modrinth"
|
||||
_ "github.com/comp500/packwiz/utils"
|
||||
)
|
||||
|
||||
|
256
modrinth/install.go
Normal file
256
modrinth/install.go
Normal file
@ -0,0 +1,256 @@
|
||||
package modrinth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/comp500/packwiz/core"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/dixonwille/wmenu.v4"
|
||||
)
|
||||
|
||||
var modSiteRegex = regexp.MustCompile("modrinth\\.com/mod/([^/]+)/?$")
|
||||
var versionSiteRegex = regexp.MustCompile("modrinth\\.com/mod/([^/]+)/version/([^/]+)/?$")
|
||||
|
||||
// installCmd represents the install command
|
||||
var installCmd = &cobra.Command{
|
||||
Use: "install [mod]",
|
||||
Short: "Install a mod from a modrinth URL, slug, ID or search",
|
||||
Aliases: []string{"add", "get"},
|
||||
Args: cobra.ArbitraryArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
pack, err := core.LoadPack()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(args) == 0 || len(args[0]) == 0 {
|
||||
fmt.Println("You must specify a mod.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// If there are more than 1 argument, go straight to searching - URLs/Slugs should not have spaces!
|
||||
if len(args) > 1 {
|
||||
err = installViaSearch(strings.Join(args, " "), pack)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed installing mod: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//Try interpreting the arg as a version url
|
||||
matches := versionSiteRegex.FindStringSubmatch(args[0])
|
||||
if matches != nil && len(matches) == 3 {
|
||||
err = installVersionById(matches[2], pack)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed installing mod: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//Try interpreting the arg as a modId or slug.
|
||||
//Modrinth transparently handles slugs/mod ids in their api; we don't have to detect which one it is.
|
||||
var modStr string
|
||||
|
||||
//Try to see if it's a site, if extract the id/slug from the url.
|
||||
//Otherwise, interpret the arg as a id/slug straight up
|
||||
matches = modSiteRegex.FindStringSubmatch(args[0])
|
||||
if matches != nil && len(matches) == 2 {
|
||||
modStr = matches[1]
|
||||
} else {
|
||||
modStr = args[0]
|
||||
}
|
||||
|
||||
mod, err := fetchMod(modStr)
|
||||
|
||||
if err == nil {
|
||||
//We found a mod with that id/slug
|
||||
err = installMod(mod, pack)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed installing mod: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
//This wasn't a valid modid/slug, try to search for it instead:
|
||||
//Don't bother to search if it looks like a url though
|
||||
if !strings.Contains(args[0], "modrinth.com") {
|
||||
err = installViaSearch(args[0], pack)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed installing mod: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func installViaSearch(query string, pack core.Pack) error {
|
||||
mcVersion, err := pack.GetMCVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := getModIdsViaSearch(query, mcVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Create menu for the user to choose the correct mod
|
||||
menu := wmenu.NewMenu("Choose a number:")
|
||||
for i, v := range results {
|
||||
menu.Option(v.Title, v, i == 0, nil)
|
||||
}
|
||||
menu.Option("Cancel", nil, false, nil)
|
||||
|
||||
menu.Action(func(menuRes []wmenu.Opt) error {
|
||||
if len(menuRes) != 1 || menuRes[0].Value == nil {
|
||||
return errors.New("Cancelled!")
|
||||
}
|
||||
|
||||
//Get the selected mod
|
||||
selectedMod, ok := menuRes[0].Value.(ModResult)
|
||||
if !ok {
|
||||
return errors.New("error converting interface from wmenu")
|
||||
}
|
||||
|
||||
//Install the selected mod
|
||||
modId := strings.TrimPrefix(selectedMod.ModID, "local-")
|
||||
|
||||
mod, err := fetchMod(modId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return installMod(mod, pack)
|
||||
})
|
||||
|
||||
return menu.Run()
|
||||
}
|
||||
|
||||
func installMod(mod Mod, pack core.Pack) error {
|
||||
fmt.Printf("Found mod %s: '%s'.\n", mod.Title, mod.Description)
|
||||
|
||||
latestVersion, err := getLatestVersion(mod.ID, pack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if latestVersion.ID == "" {
|
||||
return errors.New("mod is not available for this minecraft version or mod loader")
|
||||
}
|
||||
|
||||
return installVersion(mod, latestVersion, pack)
|
||||
}
|
||||
|
||||
func installVersion(mod Mod, version Version, pack core.Pack) error {
|
||||
var files = version.Files
|
||||
|
||||
if len(files) == 0 {
|
||||
return errors.New("version doesn't have any files attached")
|
||||
}
|
||||
|
||||
// TODO: add some way to allow users to pick which file to install?
|
||||
var file = files[0]
|
||||
// Prefer the primary file
|
||||
for _, v := range files {
|
||||
if v.Primary {
|
||||
file = v
|
||||
}
|
||||
}
|
||||
|
||||
//Install the file
|
||||
fmt.Printf("Installing %s from version %s\n", file.Filename, version.VersionNumber)
|
||||
index, err := pack.LoadIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateMap := make(map[string]map[string]interface{})
|
||||
|
||||
updateMap["modrinth"], err = mrUpdateData{
|
||||
ModID: mod.ID,
|
||||
InstalledVersion: version.ID,
|
||||
}.ToMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
side := mod.getSide()
|
||||
if side == "" {
|
||||
return errors.New("version doesn't have a side that's supported. Server: " + mod.ServerSide + " Client: " + mod.ClientSide)
|
||||
}
|
||||
|
||||
algorithm, hash := file.getBestHash()
|
||||
if algorithm == "" {
|
||||
return errors.New("file doesn't have a hash")
|
||||
}
|
||||
|
||||
modMeta := core.Mod{
|
||||
Name: mod.Title,
|
||||
FileName: file.Filename,
|
||||
Side: side,
|
||||
Download: core.ModDownload{
|
||||
URL: file.Url,
|
||||
HashFormat: algorithm,
|
||||
Hash: hash,
|
||||
},
|
||||
Update: updateMap,
|
||||
}
|
||||
var path string
|
||||
if mod.Slug != "" {
|
||||
path = modMeta.SetMetaName(mod.Slug)
|
||||
} else {
|
||||
path = modMeta.SetMetaName(mod.Title)
|
||||
}
|
||||
|
||||
// If the file already exists, this will overwrite it!!!
|
||||
// TODO: Should this be improved?
|
||||
// Current strategy is to go ahead and do stuff without asking, with the assumption that you are using
|
||||
// VCS anyway.
|
||||
|
||||
format, hash, err := modMeta.Write()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = index.RefreshFileWithHash(path, format, hash, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = index.Write()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = pack.UpdateIndexHash()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = pack.Write()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installVersionById(versionId string, pack core.Pack) error {
|
||||
version, err := fetchVersion(versionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mod, err := fetchMod(version.ModID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return installVersion(mod, version, pack)
|
||||
}
|
||||
|
||||
func init() {
|
||||
modrinthCmd.AddCommand(installCmd)
|
||||
}
|
331
modrinth/modrinth.go
Normal file
331
modrinth/modrinth.go
Normal file
@ -0,0 +1,331 @@
|
||||
package modrinth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/comp500/packwiz/cmd"
|
||||
"github.com/comp500/packwiz/core"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
const modrinthApiUrl = "https://api.modrinth.com/api/v1/"
|
||||
|
||||
var modrinthApiUrlParsed, _ = url.Parse(modrinthApiUrl)
|
||||
|
||||
var modrinthCmd = &cobra.Command{
|
||||
Use: "modrinth",
|
||||
Aliases: []string{"mr"},
|
||||
Short: "Manage modrinth-based mods",
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.Add(modrinthCmd)
|
||||
core.Updaters["modrinth"] = mrUpdater{}
|
||||
}
|
||||
|
||||
type License struct {
|
||||
Id string `json:"id"` //The license id of a mod, retrieved from the licenses get route
|
||||
Name string `json:"name"` //The long for name of a license
|
||||
Url string `json:"url"` //The URL to this license
|
||||
}
|
||||
|
||||
type Mod struct {
|
||||
ID string `json:"id"` //The ID of the mod, encoded as a base62 string
|
||||
Slug string `json:"slug"` //The slug of a mod, used for vanity URLs
|
||||
Team string `json:"team"` //The id of the team that has ownership of this mod
|
||||
Title string `json:"title"` //The title or name of the mod
|
||||
Description string `json:"description"` //A short description of the mod
|
||||
Body string `json:"body"` //A long form description of the mod.
|
||||
BodyUrl string `json:"body_url"` //DEPRECATED The link to the long description of the mod (Optional)
|
||||
Published string `json:"published"` //The date at which the mod was first published
|
||||
Updated string `json:"updated"` //The date at which the mod was updated
|
||||
Status string `json:"status"` //The status of the mod - approved, rejected, draft, unlisted, processing, or unknown
|
||||
License struct { //The license of the mod
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
} `json:"license"`
|
||||
ClientSide string `json:"client_side"` //The support range for the client mod - required, optional, unsupported, or unknown
|
||||
ServerSide string `json:"server_side"` //The support range for the server mod - required, optional, unsupported, or unknown
|
||||
Downloads int `json:"downloads"` //The total number of downloads the mod has
|
||||
Categories []string `json:"categories"` //A list of the categories that the mod is in
|
||||
Versions []string `json:"versions"` //A list of ids for versions of the mod
|
||||
IconUrl string `json:"icon_url"` //The URL of the icon of the mod (Optional)
|
||||
IssuesUrl string `json:"issues_url"` //An optional link to where to submit bugs or issues with the mod (Optional)
|
||||
SourceUrl string `json:"source_url"` //An optional link to the source code for the mod (Optional)
|
||||
WikiUrl string `json:"wiki_url"` //An optional link to the mod's wiki page or other relevant information (Optional)
|
||||
DiscordUrl string `json:"discord_url"` //An optional link to the mod's discord (Optional)
|
||||
}
|
||||
|
||||
type ModResult struct {
|
||||
ModID string `json:"mod_id"` //The id of the mod; prefixed with local-
|
||||
ProjectType string `json:"project_id"` //The project type of the mod
|
||||
Author string `json:"author"` //The username of the author of the mod
|
||||
Title string `json:"title"` //The name of the mod
|
||||
Description string `json:"description"` //A short description of the mod
|
||||
Categories []string `json:"categories"` //A list of the categories the mod is in
|
||||
Versions []string `json:"versions"` //A list of the minecraft versions supported by the mod
|
||||
Downloads int `json:"downloads"` //The total number of downloads for the mod
|
||||
PageUrl string `json:"page_url"` //A link to the mod's main page;
|
||||
IconUrl string `json:"icon_url"` //The url of the mod's icon
|
||||
AuthorUrl string `json:"author_url"` //The url of the mod's author
|
||||
DateCreated string `json:"date_created"` //The date that the mod was originally created
|
||||
DateModified string `json:"date_modified"` //The date that the mod was last modified
|
||||
LatestVersion string `json:"latest_version"` //The latest version of minecraft that this mod supports
|
||||
License string `json:"license"` //The id of the license this mod follows
|
||||
ClientSide string `json:"client_side"` //The side type id that this mod is on the client
|
||||
ServerSide string `json:"server_side"` //The side type id that this mod is on the server
|
||||
Host string `json:"host"` //The host that this mod is from, always modrinth
|
||||
}
|
||||
|
||||
type ModSearchResult struct {
|
||||
Hits []ModResult `json:"hits"` //The list of results
|
||||
Offset int `json:"offset"` //The number of results that were skipped by the query
|
||||
Limit int `json:"limit"` //The number of mods returned by the query
|
||||
TotalHits int `json:"total_hits"` //The total number of mods that the query found
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
ID string `json:"id"` //The ID of the version, encoded as a base62 string
|
||||
ModID string `json:"mod_id"` //The ID of the mod this version is for
|
||||
AuthorId string `json:"author_id"` //The ID of the author who published this version
|
||||
Featured bool `json:"featured"` //Whether the version is featured or not
|
||||
Name string `json:"name"` //The name of this version
|
||||
VersionNumber string `json:"version_number"` //The version number. Ideally will follow semantic versioning
|
||||
Changelog string `json:"changelog"` //The changelog for this version of the mod. (Optional)
|
||||
DatePublished string `json:"date_published"` //The date that this version was published
|
||||
Downloads int `json:"downloads"` //The number of downloads this specific version has
|
||||
VersionType string `json:"version_type"` //The type of the release - alpha, beta, or release
|
||||
Files []VersionFile `json:"files"` //A list of files available for download for this version
|
||||
Dependencies []string `json:"dependencies"` //A list of specific versions of mods that this version depends on
|
||||
GameVersions []string `json:"game_versions"` //A list of versions of Minecraft that this version of the mod supports
|
||||
Loaders []string `json:"loaders"` //The mod loaders that this version supports
|
||||
}
|
||||
|
||||
type VersionFile struct {
|
||||
Hashes map[string]string //A map of hashes of the file. The key is the hashing algorithm and the value is the string version of the hash.
|
||||
Url string //A direct link to the file
|
||||
Filename string //The name of the file
|
||||
Primary bool // Is the file the primary file?
|
||||
}
|
||||
|
||||
func getModIdsViaSearch(query string, version string) ([]ModResult, error) {
|
||||
baseUrl := *modrinthApiUrlParsed
|
||||
baseUrl.Path += "mod"
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("limit", "5")
|
||||
params.Add("index", "relevance")
|
||||
params.Add("facets", "[[\"versions:"+version+"\"]]")
|
||||
params.Add("query", query)
|
||||
|
||||
baseUrl.RawQuery = params.Encode()
|
||||
|
||||
resp, err := http.Get(baseUrl.String())
|
||||
if err != nil {
|
||||
return []ModResult{}, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return []ModResult{}, err
|
||||
}
|
||||
|
||||
var result ModSearchResult
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return []ModResult{}, err
|
||||
}
|
||||
|
||||
if result.TotalHits <= 0 {
|
||||
return []ModResult{}, errors.New("Couldn't find that mod. Is it available for this version?")
|
||||
}
|
||||
|
||||
return result.Hits, nil
|
||||
}
|
||||
|
||||
func getLatestVersion(modID string, pack core.Pack) (Version, error) {
|
||||
mcVersion, err := pack.GetMCVersion()
|
||||
if err != nil {
|
||||
return Version{}, err
|
||||
}
|
||||
|
||||
loader := getLoader(pack)
|
||||
|
||||
baseUrl := modrinthApiUrlParsed
|
||||
baseUrl.Path += "mod/"
|
||||
baseUrl.Path += modID
|
||||
baseUrl.Path += "/version"
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("game_versions", "[\""+mcVersion+"\"]")
|
||||
if loader != "any" {
|
||||
params.Add("loaders", "[\""+loader+"\"]")
|
||||
}
|
||||
|
||||
baseUrl.RawQuery = params.Encode()
|
||||
|
||||
resp, err := http.Get(baseUrl.String())
|
||||
if err != nil {
|
||||
return Version{}, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return Version{}, errors.New("couldn't find mod: " + modID)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return Version{}, err
|
||||
}
|
||||
|
||||
var result []Version
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return Version{}, err
|
||||
}
|
||||
|
||||
var latestValidVersion Version
|
||||
for _, v := range result {
|
||||
var semverCompare = semver.Compare(v.VersionNumber, latestValidVersion.VersionNumber)
|
||||
if semverCompare == 0 {
|
||||
//Semver is equal, compare date instead
|
||||
vDate, _ := time.Parse(time.RFC3339Nano, v.DatePublished)
|
||||
latestDate, _ := time.Parse(time.RFC3339Nano, latestValidVersion.DatePublished)
|
||||
if vDate.After(latestDate) {
|
||||
latestValidVersion = v
|
||||
}
|
||||
} else if semverCompare == 1 {
|
||||
latestValidVersion = v
|
||||
}
|
||||
}
|
||||
|
||||
return latestValidVersion, nil
|
||||
}
|
||||
|
||||
func fetchMod(modID string) (Mod, error) {
|
||||
var mod Mod
|
||||
|
||||
resp, err := http.Get(modrinthApiUrl + "mod/" + modID)
|
||||
if err != nil {
|
||||
return mod, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return mod, errors.New("couldn't find mod: " + modID)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return mod, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &mod)
|
||||
if err != nil {
|
||||
return mod, err
|
||||
}
|
||||
|
||||
if mod.ID == "" {
|
||||
return mod, errors.New("invalid json whilst fetching mod: " + modID)
|
||||
}
|
||||
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
func fetchVersion(versionId string) (Version, error) {
|
||||
var version Version
|
||||
|
||||
resp, err := http.Get(modrinthApiUrl + "version/" + versionId)
|
||||
if err != nil {
|
||||
return version, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return version, errors.New("couldn't find version: " + versionId)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return version, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &version)
|
||||
if err != nil {
|
||||
return version, err
|
||||
}
|
||||
|
||||
if version.ID == "" {
|
||||
return version, errors.New("invalid json whilst fetching version: " + versionId)
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (mod Mod) getSide() string {
|
||||
server := shouldDownloadOnSide(mod.ServerSide)
|
||||
client := shouldDownloadOnSide(mod.ClientSide)
|
||||
|
||||
if server && client {
|
||||
return core.UniversalSide
|
||||
} else if server {
|
||||
return core.ServerSide
|
||||
} else if client {
|
||||
return core.ClientSide
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func shouldDownloadOnSide(side string) bool {
|
||||
return side == "required" || side == "optional"
|
||||
}
|
||||
|
||||
func (v VersionFile) getBestHash() (string, string) {
|
||||
//try preferred hashes first
|
||||
val, exists := v.Hashes["sha256"]
|
||||
if exists {
|
||||
return "sha256", val
|
||||
}
|
||||
val, exists = v.Hashes["murmur2"]
|
||||
if exists {
|
||||
return "murmur2", val
|
||||
}
|
||||
val, exists = v.Hashes["sha512"]
|
||||
if exists {
|
||||
return "sha512", val
|
||||
}
|
||||
|
||||
//none of the preferred hashes are present, just get the first one
|
||||
for key, val := range v.Hashes {
|
||||
return key, val
|
||||
}
|
||||
|
||||
//No hashes were present
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func getLoader(pack core.Pack) string {
|
||||
dependencies := pack.Versions
|
||||
|
||||
_, hasFabric := dependencies["fabric"]
|
||||
_, hasForge := dependencies["forge"]
|
||||
if hasFabric && hasForge {
|
||||
return "any"
|
||||
} else if hasFabric {
|
||||
return "fabric"
|
||||
} else if hasForge {
|
||||
return "forge"
|
||||
} else {
|
||||
return "any"
|
||||
}
|
||||
}
|
110
modrinth/updater.go
Normal file
110
modrinth/updater.go
Normal file
@ -0,0 +1,110 @@
|
||||
package modrinth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/comp500/packwiz/core"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type mrUpdateData struct {
|
||||
ModID string `mapstructure:"mod-id"`
|
||||
InstalledVersion string `mapstructure:"version"`
|
||||
}
|
||||
|
||||
func (u mrUpdateData) ToMap() (map[string]interface{}, error) {
|
||||
newMap := make(map[string]interface{})
|
||||
err := mapstructure.Decode(u, &newMap)
|
||||
return newMap, err
|
||||
}
|
||||
|
||||
type mrUpdater struct{}
|
||||
|
||||
func (u mrUpdater) ParseUpdate(updateUnparsed map[string]interface{}) (interface{}, error) {
|
||||
var updateData mrUpdateData
|
||||
err := mapstructure.Decode(updateUnparsed, &updateData)
|
||||
return updateData, err
|
||||
}
|
||||
|
||||
type cachedStateStore struct {
|
||||
ModID string
|
||||
Version Version
|
||||
}
|
||||
|
||||
func (u mrUpdater) CheckUpdate(mods []core.Mod, mcVersion string) ([]core.UpdateCheck, error) {
|
||||
results := make([]core.UpdateCheck, len(mods))
|
||||
|
||||
pack, err := core.LoadPack()
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
for i, mod := range mods {
|
||||
rawData, ok := mod.GetParsedUpdateData("modrinth")
|
||||
if !ok {
|
||||
results[i] = core.UpdateCheck{Error: errors.New("couldn't parse mod data")}
|
||||
continue
|
||||
}
|
||||
|
||||
data := rawData.(mrUpdateData)
|
||||
|
||||
newVersion, err := getLatestVersion(data.ModID, pack)
|
||||
if err != nil {
|
||||
results[i] = core.UpdateCheck{Error: err}
|
||||
continue
|
||||
}
|
||||
|
||||
if newVersion.ID == "" { //There is no version available for this minecraft version or loader.
|
||||
results[i] = core.UpdateCheck{UpdateAvailable: false}
|
||||
continue
|
||||
}
|
||||
|
||||
if newVersion.ID == data.InstalledVersion { //The latest version from the site is the same as the installed one
|
||||
results[i] = core.UpdateCheck{UpdateAvailable: false}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(newVersion.Files) == 0 {
|
||||
results[i] = core.UpdateCheck{Error: errors.New("new version doesn't have any files")}
|
||||
continue
|
||||
}
|
||||
|
||||
results[i] = core.UpdateCheck{
|
||||
UpdateAvailable: true,
|
||||
UpdateString: mod.FileName + " -> " + newVersion.Files[0].Filename,
|
||||
CachedState: cachedStateStore{data.ModID, newVersion},
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (u mrUpdater) DoUpdate(mods []*core.Mod, cachedState []interface{}) error {
|
||||
for i, mod := range mods {
|
||||
modState := cachedState[i].(cachedStateStore)
|
||||
var version = modState.Version
|
||||
|
||||
var file = version.Files[0]
|
||||
// Prefer the primary file
|
||||
for _, v := range version.Files {
|
||||
if v.Primary {
|
||||
file = v
|
||||
}
|
||||
}
|
||||
|
||||
algorithm, hash := file.getBestHash()
|
||||
if algorithm == "" {
|
||||
return errors.New("file for mod " + mod.Name + " doesn't have a hash")
|
||||
}
|
||||
|
||||
mod.FileName = file.Filename
|
||||
mod.Download = core.ModDownload{
|
||||
URL: file.Url,
|
||||
HashFormat: algorithm,
|
||||
Hash: hash,
|
||||
}
|
||||
mod.Update["modrinth"]["version"] = version.ID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user