From 044c34e07cdd060c1b0a3bb8b2016a437f9a60b1 Mon Sep 17 00:00:00 2001
From: comp500 <comp500@users.noreply.github.com>
Date: Tue, 2 Aug 2022 02:53:31 +0100
Subject: [PATCH] Move to go-modrinth lib (v2 API) and always supply UA in HTTP
 requests

---
 cmd/init.go              |   3 +-
 core/download.go         |  15 +-
 core/versionutil.go      |   5 +-
 curseforge/curseforge.go |   4 +-
 curseforge/request.go    |   7 +-
 go.mod                   |   7 +-
 go.sum                   |   2 +
 modrinth/install.go      |  54 ++++----
 modrinth/modrinth.go     | 293 +++++----------------------------------
 modrinth/updater.go      |  22 ++-
 10 files changed, 97 insertions(+), 315 deletions(-)

diff --git a/cmd/init.go b/cmd/init.go
index 98d824a..f63b621 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
-	"net/http"
 	"os"
 	"path/filepath"
 	"sort"
@@ -267,7 +266,7 @@ func (m mcVersionManifest) checkValid(version string) {
 }
 
 func getValidMCVersions() (mcVersionManifest, error) {
-	res, err := http.Get("https://launchermeta.mojang.com/mc/game/version_manifest.json")
+	res, err := core.GetWithUA("https://launchermeta.mojang.com/mc/game/version_manifest.json", "application/json")
 	if err != nil {
 		return mcVersionManifest{}, err
 	}
diff --git a/core/download.go b/core/download.go
index 7073041..ffefc3e 100644
--- a/core/download.go
+++ b/core/download.go
@@ -13,6 +13,18 @@ import (
 	"strings"
 )
 
+const UserAgent = "packwiz/packwiz"
+
+func GetWithUA(url string, contentType string) (resp *http.Response, err error) {
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("User-Agent", UserAgent)
+	req.Header.Set("Accept", contentType)
+	return http.DefaultClient.Do(req)
+}
+
 const DownloadCacheImportFolder = "import"
 
 type DownloadSession interface {
@@ -143,8 +155,7 @@ func downloadNewFile(task *downloadTask, cacheFolder string, hashesToObtain []st
 	if len(hashesToObtain) > 0 {
 		var data io.ReadCloser
 		if task.url != "" {
-			resp, err := http.Get(task.url)
-			// TODO: content type, user-agent?
+			resp, err := GetWithUA(task.url, "application/octet-stream")
 			if err != nil {
 				return CompletedDownload{}, fmt.Errorf("failed to download %s: %w", task.url, err)
 			}
diff --git a/core/versionutil.go b/core/versionutil.go
index 0bed4dc..a971227 100644
--- a/core/versionutil.go
+++ b/core/versionutil.go
@@ -3,7 +3,6 @@ package core
 import (
 	"encoding/xml"
 	"errors"
-	"net/http"
 	"strings"
 )
 
@@ -53,7 +52,7 @@ var ModLoaders = map[string]ModLoaderComponent{
 
 func FetchMavenVersionList(url string) func(mcVersion string) ([]string, string, error) {
 	return func(mcVersion string) ([]string, string, error) {
-		res, err := http.Get(url)
+		res, err := GetWithUA(url, "application/xml")
 		if err != nil {
 			return []string{}, "", err
 		}
@@ -69,7 +68,7 @@ func FetchMavenVersionList(url string) func(mcVersion string) ([]string, string,
 
 func FetchMavenVersionPrefixedList(url string, friendlyName string) func(mcVersion string) ([]string, string, error) {
 	return func(mcVersion string) ([]string, string, error) {
-		res, err := http.Get(url)
+		res, err := GetWithUA(url, "application/xml")
 		if err != nil {
 			return []string{}, "", err
 		}
diff --git a/curseforge/curseforge.go b/curseforge/curseforge.go
index 8df8fe4..bdf5aee 100644
--- a/curseforge/curseforge.go
+++ b/curseforge/curseforge.go
@@ -6,7 +6,6 @@ import (
 	"github.com/spf13/viper"
 	"golang.org/x/exp/slices"
 	"io"
-	"net/http"
 	"path/filepath"
 	"regexp"
 	"strconv"
@@ -540,8 +539,7 @@ func (m *cfDownloadMetadata) GetManualDownload() (bool, core.ManualDownload) {
 }
 
 func (m *cfDownloadMetadata) DownloadFile() (io.ReadCloser, error) {
-	resp, err := http.Get(m.url)
-	// TODO: content type, user-agent?
+	resp, err := core.GetWithUA(m.url, "application/octet-stream")
 	if err != nil {
 		return nil, fmt.Errorf("failed to download %s: %w", m.url, err)
 	}
diff --git a/curseforge/request.go b/curseforge/request.go
index 977b1fa..efb154d 100644
--- a/curseforge/request.go
+++ b/curseforge/request.go
@@ -5,6 +5,7 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
+	"github.com/packwiz/packwiz/core"
 	"io"
 	"net/http"
 	"net/url"
@@ -40,8 +41,7 @@ func (c *cfApiClient) makeGet(endpoint string) (*http.Response, error) {
 		return nil, err
 	}
 
-	// TODO: make this configurable application-wide
-	req.Header.Set("User-Agent", "packwiz/packwiz client")
+	req.Header.Set("User-Agent", core.UserAgent)
 	req.Header.Set("Accept", "application/json")
 	if cfApiKey == "" {
 		cfApiKey = decodeDefaultKey()
@@ -65,8 +65,7 @@ func (c *cfApiClient) makePost(endpoint string, body io.Reader) (*http.Response,
 		return nil, err
 	}
 
-	// TODO: make this configurable application-wide
-	req.Header.Set("User-Agent", "packwiz/packwiz client")
+	req.Header.Set("User-Agent", core.UserAgent)
 	req.Header.Set("Accept", "application/json")
 	req.Header.Set("Content-Type", "application/json")
 	if cfApiKey == "" {
diff --git a/go.mod b/go.mod
index 02f8b9b..921bf2b 100644
--- a/go.mod
+++ b/go.mod
@@ -26,7 +26,11 @@ require (
 	gopkg.in/dixonwille/wmenu.v4 v4.0.2
 )
 
-require golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd
+require (
+	codeberg.org/jmansfield/go-modrinth v0.4.1
+	github.com/spf13/pflag v1.0.5
+	golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd
+)
 
 require (
 	github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
@@ -40,7 +44,6 @@ require (
 	github.com/spf13/afero v1.8.2 // indirect
 	github.com/spf13/cast v1.4.1 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
 	gopkg.in/ini.v1 v1.66.4 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index 54b991e..eb574b2 100644
--- a/go.sum
+++ b/go.sum
@@ -35,6 +35,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
+codeberg.org/jmansfield/go-modrinth v0.4.1 h1:uCWqjep41jplMLLyarQ6rvD+4FyFIBsa5UcPfZWOGTo=
+codeberg.org/jmansfield/go-modrinth v0.4.1/go.mod h1:feVF2NqtWdzIpCMgUT/Q/Wb8CpEVpi64m4TJXB+ejq0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
diff --git a/modrinth/install.go b/modrinth/install.go
index e9c32dc..c40a8e3 100644
--- a/modrinth/install.go
+++ b/modrinth/install.go
@@ -1,6 +1,7 @@
 package modrinth
 
 import (
+	modrinthApi "codeberg.org/jmansfield/go-modrinth/modrinth"
 	"errors"
 	"fmt"
 	"github.com/spf13/viper"
@@ -69,7 +70,7 @@ var installCmd = &cobra.Command{
 			modStr = args[0]
 		}
 
-		mod, err := fetchMod(modStr)
+		mod, err := mrDefaultClient.Projects.Get(modStr)
 
 		if err == nil {
 			//We found a mod with that id/slug
@@ -111,7 +112,8 @@ func installViaSearch(query string, pack core.Pack) error {
 	menu := wmenu.NewMenu("Choose a number:")
 	menu.Option("Cancel", nil, false, nil)
 	for i, v := range results {
-		menu.Option(v.Title, v, i == 0, nil)
+		// Should be non-nil (Title is a required field)
+		menu.Option(*v.Title, v, i == 0, nil)
 	}
 
 	menu.Action(func(menuRes []wmenu.Opt) error {
@@ -120,15 +122,13 @@ func installViaSearch(query string, pack core.Pack) error {
 		}
 
 		//Get the selected mod
-		selectedMod, ok := menuRes[0].Value.(ModResult)
+		selectedMod, ok := menuRes[0].Value.(*modrinthApi.SearchResult)
 		if !ok {
 			return errors.New("error converting interface from wmenu")
 		}
 
 		//Install the selected mod
-		modId := strings.TrimPrefix(selectedMod.ModID, "local-")
-
-		mod, err := fetchMod(modId)
+		mod, err := mrDefaultClient.Projects.Get(*selectedMod.ProjectID)
 		if err != nil {
 			return err
 		}
@@ -139,21 +139,21 @@ func installViaSearch(query string, pack core.Pack) error {
 	return menu.Run()
 }
 
-func installMod(mod Mod, pack core.Pack) error {
-	fmt.Printf("Found mod %s: '%s'.\n", mod.Title, mod.Description)
+func installMod(mod *modrinthApi.Project, pack core.Pack) error {
+	fmt.Printf("Found mod %s: '%s'.\n", *mod.Title, *mod.Description)
 
-	latestVersion, err := getLatestVersion(mod.ID, pack)
+	latestVersion, err := getLatestVersion(*mod.ID, pack)
 	if err != nil {
 		return fmt.Errorf("failed to get latest version: %v", err)
 	}
-	if latestVersion.ID == "" {
+	if latestVersion.ID == nil {
 		return errors.New("mod is not available for this Minecraft version (use the acceptable-game-versions option to accept more) or mod loader")
 	}
 
 	return installVersion(mod, latestVersion, pack)
 }
 
-func installVersion(mod Mod, version Version, pack core.Pack) error {
+func installVersion(mod *modrinthApi.Project, version *modrinthApi.Version, pack core.Pack) error {
 	var files = version.Files
 
 	if len(files) == 0 {
@@ -164,13 +164,13 @@ func installVersion(mod Mod, version Version, pack core.Pack) error {
 	var file = files[0]
 	// Prefer the primary file
 	for _, v := range files {
-		if v.Primary {
+		if *v.Primary {
 			file = v
 		}
 	}
 
 	//Install the file
-	fmt.Printf("Installing %s from version %s\n", file.Filename, version.VersionNumber)
+	fmt.Printf("Installing %s from version %s\n", *file.Filename, *version.VersionNumber)
 	index, err := pack.LoadIndex()
 	if err != nil {
 		return err
@@ -179,29 +179,29 @@ func installVersion(mod Mod, version Version, pack core.Pack) error {
 	updateMap := make(map[string]map[string]interface{})
 
 	updateMap["modrinth"], err = mrUpdateData{
-		ModID:            mod.ID,
-		InstalledVersion: version.ID,
+		ModID:            *mod.ID,
+		InstalledVersion: *version.ID,
 	}.ToMap()
 	if err != nil {
 		return err
 	}
 
-	side := mod.getSide()
+	side := getSide(mod)
 	if side == "" {
-		return errors.New("version doesn't have a side that's supported. Server: " + mod.ServerSide + " Client: " + mod.ClientSide)
+		return errors.New("version doesn't have a side that's supported. Server: " + *mod.ServerSide + " Client: " + *mod.ClientSide)
 	}
 
-	algorithm, hash := file.getBestHash()
+	algorithm, hash := getBestHash(file)
 	if algorithm == "" {
 		return errors.New("file doesn't have a hash")
 	}
 
 	modMeta := core.Mod{
-		Name:     mod.Title,
-		FileName: file.Filename,
+		Name:     *mod.Title,
+		FileName: *file.Filename,
 		Side:     side,
 		Download: core.ModDownload{
-			URL:        file.Url,
+			URL:        *file.URL,
 			HashFormat: algorithm,
 			Hash:       hash,
 		},
@@ -212,10 +212,10 @@ func installVersion(mod Mod, version Version, pack core.Pack) error {
 	if folder == "" {
 		folder = "mods"
 	}
-	if mod.Slug != "" {
-		path = modMeta.SetMetaPath(filepath.Join(viper.GetString("meta-folder-base"), folder, mod.Slug+core.MetaExtension))
+	if mod.Slug != nil {
+		path = modMeta.SetMetaPath(filepath.Join(viper.GetString("meta-folder-base"), folder, *mod.Slug+core.MetaExtension))
 	} else {
-		path = modMeta.SetMetaPath(filepath.Join(viper.GetString("meta-folder-base"), folder, mod.Title+core.MetaExtension))
+		path = modMeta.SetMetaPath(filepath.Join(viper.GetString("meta-folder-base"), folder, *mod.Title+core.MetaExtension))
 	}
 
 	// If the file already exists, this will overwrite it!!!
@@ -247,14 +247,14 @@ func installVersion(mod Mod, version Version, pack core.Pack) error {
 }
 
 func installVersionById(versionId string, pack core.Pack) error {
-	version, err := fetchVersion(versionId)
+	version, err := mrDefaultClient.Versions.Get(versionId)
 	if err != nil {
 		return fmt.Errorf("failed to fetch version %s: %v", versionId, err)
 	}
 
-	mod, err := fetchMod(version.ModID)
+	mod, err := mrDefaultClient.Projects.Get(*version.ProjectID)
 	if err != nil {
-		return fmt.Errorf("failed to fetch mod %s: %v", version.ModID, err)
+		return fmt.Errorf("failed to fetch mod %s: %v", *version.ProjectID, err)
 	}
 
 	return installVersion(mod, version, pack)
diff --git a/modrinth/modrinth.go b/modrinth/modrinth.go
index 51f3341..9c2a7f0 100644
--- a/modrinth/modrinth.go
+++ b/modrinth/modrinth.go
@@ -1,227 +1,72 @@
 package modrinth
 
 import (
-	"encoding/json"
+	modrinthApi "codeberg.org/jmansfield/go-modrinth/modrinth"
 	"errors"
-	"fmt"
-	"github.com/spf13/viper"
-	"golang.org/x/exp/slices"
-	"io/ioutil"
-	"net/http"
-	"net/url"
-	"time"
-
 	"github.com/Masterminds/semver/v3"
 	"github.com/packwiz/packwiz/cmd"
 	"github.com/packwiz/packwiz/core"
 	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+	"golang.org/x/exp/slices"
+	"net/http"
 )
 
-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",
 }
 
+var mrDefaultClient = modrinthApi.NewClient(&http.Client{})
+
 func init() {
 	cmd.Add(modrinthCmd)
 	core.Updaters["modrinth"] = mrUpdater{}
+
+	mrDefaultClient.UserAgent = core.UserAgent
 }
 
-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  uint32   `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     uint32   `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    uint32      `json:"offset"`     //The number of results that were skipped by the query
-	Limit     uint32      `json:"limit"`      //The number of mods returned by the query
-	TotalHits uint32      `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     uint32        `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, versions []string) ([]ModResult, error) {
-	baseUrl := *modrinthApiUrlParsed
-	baseUrl.Path += "mod"
-
-	params := url.Values{}
-	params.Add("limit", "5")
-	params.Add("index", "relevance")
+func getModIdsViaSearch(query string, versions []string) ([]*modrinthApi.SearchResult, error) {
 	facets := make([]string, 0)
 	for _, v := range versions {
 		facets = append(facets, "versions:"+v)
 	}
-	facetsEncoded, err := json.Marshal(facets)
+
+	res, err := mrDefaultClient.Projects.Search(&modrinthApi.SearchOptions{
+		Limit: 5,
+		Index: "relevance",
+		// Filters by mod since currently only mods and modpacks are supported by Modrinth
+		Facets: [][]string{facets, {"project_type:mod"}},
+		Query:  query,
+	})
+
 	if err != nil {
-		return []ModResult{}, err
+		return nil, err
 	}
-	params.Add("facets", "["+string(facetsEncoded)+"]")
-	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
+	return res.Hits, nil
 }
 
-func getLatestVersion(modID string, pack core.Pack) (Version, error) {
+func getLatestVersion(modID string, pack core.Pack) (*modrinthApi.Version, error) {
 	mcVersion, err := pack.GetMCVersion()
 	if err != nil {
-		return Version{}, err
+		return nil, err
 	}
 	gameVersions := append([]string{mcVersion}, viper.GetStringSlice("acceptable-game-versions")...)
-	gameVersionsEncoded, err := json.Marshal(gameVersions)
-	if err != nil {
-		return Version{}, err
-	}
 
-	loadersEncoded, err := json.Marshal(pack.GetLoaders())
-	if err != nil {
-		return Version{}, err
-	}
-
-	baseUrl := *modrinthApiUrlParsed
-	baseUrl.Path += "mod/"
-	baseUrl.Path += modID
-	baseUrl.Path += "/version"
-
-	params := url.Values{}
-	params.Add("game_versions", string(gameVersionsEncoded))
-	params.Add("loaders", string(loadersEncoded))
-
-	baseUrl.RawQuery = params.Encode()
-
-	resp, err := http.Get(baseUrl.String())
-	if err != nil {
-		return Version{}, err
-	}
-
-	if resp.StatusCode == 404 {
-		return Version{}, fmt.Errorf("mod not found (for URL %v)", baseUrl)
-	}
-
-	if resp.StatusCode != 200 {
-		return Version{}, fmt.Errorf("invalid response status %v for URL %v", resp.Status, baseUrl)
-	}
-
-	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
-	}
+	result, err := mrDefaultClient.Versions.ListVersions(modID, modrinthApi.ListVersionsOptions{
+		GameVersions: gameVersions,
+		Loaders:      pack.GetLoaders(),
+	})
 
 	if len(result) == 0 {
-		return Version{}, errors.New("no valid versions found")
+		return nil, errors.New("no valid versions found")
 	}
 
 	latestValidVersion := result[0]
 	for _, v := range result[1:] {
-		currVersion, err1 := semver.NewVersion(v.VersionNumber)
-		latestVersion, err2 := semver.NewVersion(latestValidVersion.VersionNumber)
+		currVersion, err1 := semver.NewVersion(*v.VersionNumber)
+		latestVersion, err2 := semver.NewVersion(*latestValidVersion.VersionNumber)
 		var semverCompare = 0
 		// Only compare with semver if both are valid semver - otherwise compare by release date
 		if err1 == nil && err2 == nil {
@@ -236,9 +81,7 @@ func getLatestVersion(modID string, pack core.Pack) (Version, error) {
 			}
 
 			//Semver is equal, compare date instead
-			vDate, _ := time.Parse(time.RFC3339Nano, v.DatePublished)
-			latestDate, _ := time.Parse(time.RFC3339Nano, latestValidVersion.DatePublished)
-			if vDate.After(latestDate) {
+			if v.DatePublished.After(*latestValidVersion.DatePublished) {
 				latestValidVersion = v
 			}
 		} else if semverCompare == 1 {
@@ -249,77 +92,9 @@ func getLatestVersion(modID string, pack core.Pack) (Version, error) {
 	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, fmt.Errorf("mod not found (for URL %v)", modrinthApiUrl+"mod/"+modID)
-	}
-
-	if resp.StatusCode != 200 {
-		return mod, fmt.Errorf("invalid response status %v for URL %v", resp.Status, modrinthApiUrl+"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, fmt.Errorf("version not found (for URL %v)", modrinthApiUrl+"version/"+versionId)
-	}
-
-	if resp.StatusCode != 200 {
-		return version, fmt.Errorf("invalid response status %v for URL %v", resp.Status, modrinthApiUrl+"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)
+func getSide(mod *modrinthApi.Project) string {
+	server := shouldDownloadOnSide(*mod.ServerSide)
+	client := shouldDownloadOnSide(*mod.ClientSide)
 
 	if server && client {
 		return core.UniversalSide
@@ -336,7 +111,7 @@ func shouldDownloadOnSide(side string) bool {
 	return side == "required" || side == "optional"
 }
 
-func (v VersionFile) getBestHash() (string, string) {
+func getBestHash(v *modrinthApi.File) (string, string) {
 	// Try preferred hashes first; SHA1 is first as it is required for Modrinth pack exporting
 	val, exists := v.Hashes["sha1"]
 	if exists {
diff --git a/modrinth/updater.go b/modrinth/updater.go
index e927ccb..1c1f993 100644
--- a/modrinth/updater.go
+++ b/modrinth/updater.go
@@ -1,6 +1,7 @@
 package modrinth
 
 import (
+	modrinthApi "codeberg.org/jmansfield/go-modrinth/modrinth"
 	"errors"
 	"fmt"
 
@@ -29,7 +30,7 @@ func (u mrUpdater) ParseUpdate(updateUnparsed map[string]interface{}) (interface
 
 type cachedStateStore struct {
 	ModID   string
-	Version Version
+	Version *modrinthApi.Version
 }
 
 func (u mrUpdater) CheckUpdate(mods []core.Mod, mcVersion string, pack core.Pack) ([]core.UpdateCheck, error) {
@@ -50,12 +51,7 @@ func (u mrUpdater) CheckUpdate(mods []core.Mod, mcVersion string, pack core.Pack
 			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
+		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
 		}
@@ -68,14 +64,14 @@ func (u mrUpdater) CheckUpdate(mods []core.Mod, mcVersion string, pack core.Pack
 		newFilename := newVersion.Files[0].Filename
 		// Prefer the primary file
 		for _, v := range newVersion.Files {
-			if v.Primary {
+			if *v.Primary {
 				newFilename = v.Filename
 			}
 		}
 
 		results[i] = core.UpdateCheck{
 			UpdateAvailable: true,
-			UpdateString:    mod.FileName + " -> " + newFilename,
+			UpdateString:    mod.FileName + " -> " + *newFilename,
 			CachedState:     cachedStateStore{data.ModID, newVersion},
 		}
 	}
@@ -91,19 +87,19 @@ func (u mrUpdater) DoUpdate(mods []*core.Mod, cachedState []interface{}) error {
 		var file = version.Files[0]
 		// Prefer the primary file
 		for _, v := range version.Files {
-			if v.Primary {
+			if *v.Primary {
 				file = v
 			}
 		}
 
-		algorithm, hash := file.getBestHash()
+		algorithm, hash := getBestHash(file)
 		if algorithm == "" {
 			return errors.New("file for mod " + mod.Name + " doesn't have a hash")
 		}
 
-		mod.FileName = file.Filename
+		mod.FileName = *file.Filename
 		mod.Download = core.ModDownload{
-			URL:        file.Url,
+			URL:        *file.URL,
 			HashFormat: algorithm,
 			Hash:       hash,
 		}