├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.bat ├── curseapi ├── cache.go ├── curseapi.go ├── http.go └── json.go ├── go.mod ├── go.sum ├── main.go └── web ├── genhtml.go ├── html.go ├── html ├── footer.html ├── head.html ├── history.html ├── index.html ├── page.html └── search.html └── web.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # ******** NOTE ******** 12 | 13 | name: "CodeQL" 14 | 15 | on: 16 | push: 17 | branches: [ master ] 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: [ master ] 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze 25 | runs-on: ubuntu-latest 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | language: [ 'go' ] 31 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 32 | # Learn more: 33 | # https://docs.github.com/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 34 | 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@v2 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v1 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v1 67 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | if [ -f Gopkg.toml ]; then 29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 30 | dep ensure 31 | fi 32 | 33 | - name: Build 34 | run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o cursemodownload.exe -trimpath -ldflags "-w -s" main.go ; CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o cursemodownload -trimpath -ldflags "-w -s" main.go 35 | 36 | - name: Test 37 | run: go test -v ./... 38 | 39 | - name: Upload a Build Artifact 40 | uses: actions/upload-artifact@v2 41 | with: 42 | # A file, directory or wildcard pattern that describes what to upload 43 | path: ./cursemodownload* 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | main.exe 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 xmdhs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cursemodownload 2 | This tool uses the CurseForge API to retrieve mod download links without rehosting any files. -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | SET CGO_ENABLED=0 2 | SET GOOS=linux 3 | SET GOARCH=amd64 4 | go build -o cursemodownload -trimpath -ldflags "-w -s" main.go 5 | 6 | -------------------------------------------------------------------------------- /curseapi/cache.go: -------------------------------------------------------------------------------- 1 | package curseapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "sync" 7 | "time" 8 | 9 | "github.com/VictoriaMetrics/fastcache" 10 | ) 11 | 12 | type cache struct { 13 | f *fastcache.Cache 14 | cancel func() 15 | expdate time.Duration 16 | bfpool sync.Pool 17 | } 18 | 19 | func newcache(expdata time.Duration) *cache { 20 | c := &cache{} 21 | c.f = fastcache.New(32000000) 22 | c.expdate = expdata 23 | c.bfpool = sync.Pool{ 24 | New: func() interface{} { 25 | return bytes.NewBuffer(nil) 26 | }, 27 | } 28 | return c 29 | } 30 | 31 | func (c *cache) Close() { 32 | c.cancel() 33 | } 34 | 35 | func (c *cache) Load(key string) []byte { 36 | b := c.f.GetBig(nil, []byte(key)) 37 | if b == nil { 38 | return nil 39 | } 40 | var d int64 41 | err := binary.Read(bytes.NewReader(b[:8]), binary.BigEndian, &d) 42 | if err != nil { 43 | return nil 44 | } 45 | t := time.Unix(d, 0) 46 | if t.Before(time.Now()) { 47 | c.f.Del([]byte(key)) 48 | return nil 49 | } 50 | return b[8:] 51 | } 52 | 53 | func (c *cache) Store(key string, adate []byte) { 54 | w := c.bfpool.Get().(*bytes.Buffer) 55 | binary.Write(w, binary.BigEndian, time.Now().Add(c.expdate).Unix()) 56 | w.Write(adate) 57 | b := w.Bytes() 58 | c.f.SetBig([]byte(key), b) 59 | w.Reset() 60 | c.bfpool.Put(w) 61 | } 62 | 63 | func (c *cache) Delete(key string) { 64 | c.f.Del([]byte(key)) 65 | } 66 | -------------------------------------------------------------------------------- /curseapi/curseapi.go: -------------------------------------------------------------------------------- 1 | package curseapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "sort" 8 | "strconv" 9 | ) 10 | 11 | //From https://gaz492.github.io/TwitchAPI/ 12 | 13 | const api = `https://api.curseforge.com/v1` 14 | 15 | func Searchmod(key string, index string, sectionId int) ([]Modinfo, error) { 16 | aurl := api + `/mods/search?categoryId=0&gameId=432&index=` + index + `&pageSize=20&searchFilter=` + url.QueryEscape(key) + `&classId=` + strconv.Itoa(sectionId) + `&sortField=2&sortOrder=desc` 17 | b, err := httpcache(aurl, acache) 18 | if err != nil { 19 | return nil, fmt.Errorf("Searchmod: %w", err) 20 | } 21 | m, err := json2Modinfo(b) 22 | if err != nil { 23 | acache.Delete(aurl) 24 | return nil, fmt.Errorf("Searchmod: %w", err) 25 | } 26 | return m, nil 27 | } 28 | 29 | func FileId2downloadlink(id string) (string, error) { 30 | aurl := api + `/mods/0/file/` + id + `/download-url` 31 | b, err := httpcache(aurl, acache) 32 | if err != nil { 33 | return "", fmt.Errorf("FileId2downloadlink: %w", err) 34 | } 35 | return string(b), nil 36 | } 37 | 38 | //https://media.forgecdn.net/files/3046/220/jei-1.16.2-7.3.2.25.jar 39 | 40 | func AddonInfo(addonID string) (Modinfo, error) { 41 | aurl := api + `/mods/` + addonID 42 | b, err := httpcache(aurl, acache) 43 | if err != nil { 44 | return Modinfo{}, fmt.Errorf("AddonInfo: %w", err) 45 | } 46 | d := apidata[Modinfo]{} 47 | err = json.Unmarshal(b, &d) 48 | m := d.Data 49 | if err != nil { 50 | acache.Delete(aurl) 51 | return Modinfo{}, fmt.Errorf("AddonInfo: %w", err) 52 | } 53 | return m, nil 54 | } 55 | 56 | func Addonfiles(addonID, gameVersion string) ([]Files, error) { 57 | aurl := api + `/mods/` + addonID + `/files?pageSize=10000&gameVersion=` + gameVersion 58 | b, err := httpcache(aurl, acache) 59 | if err != nil { 60 | return nil, fmt.Errorf("Addonfiles: %w", err) 61 | } 62 | d := apidata[[]Files]{} 63 | err = json.Unmarshal(b, &d) 64 | m := d.Data 65 | if err != nil { 66 | acache.Delete(aurl) 67 | return nil, fmt.Errorf("Addonfiles: %w", err) 68 | } 69 | sort.Slice(m, func(i, j int) bool { 70 | return m[i].ID > m[j].ID 71 | }) 72 | return m, nil 73 | } 74 | -------------------------------------------------------------------------------- /curseapi/http.go: -------------------------------------------------------------------------------- 1 | package curseapi 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | "golang.org/x/sync/singleflight" 13 | ) 14 | 15 | var c = http.Client{Timeout: 10 * time.Second} 16 | 17 | var key = os.Getenv("CURSE_API_KEY") 18 | 19 | func httpget(url string) ([]byte, error) { 20 | reqs, err := http.NewRequest("GET", url, nil) 21 | if err != nil { 22 | return nil, fmt.Errorf("httpget: %w", err) 23 | } 24 | reqs.Header.Set("Accept", "*/*") 25 | reqs.Header.Set("Accept-Encoding", "gzip") 26 | reqs.Header.Set("x-api-key", key) 27 | reqs.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36") 28 | rep, err := c.Do(reqs) 29 | if rep != nil { 30 | defer rep.Body.Close() 31 | } 32 | if err != nil { 33 | return nil, fmt.Errorf("httpget: %w", err) 34 | } 35 | if rep.StatusCode != http.StatusOK { 36 | return nil, fmt.Errorf("httpget: %w", ErrHttpCode{Code: rep.StatusCode}) 37 | } 38 | var reader io.ReadCloser 39 | switch rep.Header.Get("Content-Encoding") { 40 | case "gzip": 41 | reader, err = gzip.NewReader(rep.Body) 42 | if err != nil { 43 | return nil, fmt.Errorf("httpget: %w", err) 44 | } 45 | defer reader.Close() 46 | default: 47 | reader = rep.Body 48 | } 49 | b, err := ioutil.ReadAll(reader) 50 | if err != nil { 51 | return nil, fmt.Errorf("httpget: %w", err) 52 | } 53 | return b, err 54 | } 55 | 56 | type ErrHttpCode struct { 57 | Code int 58 | } 59 | 60 | func (e ErrHttpCode) Error() string { 61 | return fmt.Sprintf("ErrHttpCode: %d", e.Code) 62 | } 63 | 64 | var acache = newcache(30 * time.Minute) 65 | 66 | var s = singleflight.Group{} 67 | 68 | func httpcache(url string, acache *cache) ([]byte, error) { 69 | b := acache.Load(url) 70 | if b != nil { 71 | return b, nil 72 | } 73 | t, err, _ := s.Do(url, func() (interface{}, error) { 74 | b, err := httpget(url) 75 | if err != nil { 76 | return nil, err 77 | } 78 | acache.Store(url, b) 79 | return b, nil 80 | }) 81 | if err != nil { 82 | return nil, fmt.Errorf("httpcache: %w", err) 83 | } 84 | b = t.([]byte) 85 | return b, nil 86 | } 87 | -------------------------------------------------------------------------------- /curseapi/json.go: -------------------------------------------------------------------------------- 1 | package curseapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type apidata[v any] struct { 9 | Data v `json:"data"` 10 | } 11 | 12 | type Modinfo struct { 13 | Name string `json:"name"` 14 | ID int `json:"id"` 15 | GameVersionLatestFiles []GameVersionFiles `json:"latestFilesIndexes"` 16 | Links struct { 17 | WebsiteUrl string `json:"websiteUrl"` 18 | } `json:"links"` 19 | Summary string `json:"summary"` 20 | } 21 | 22 | type GameVersionFiles struct { 23 | GameVersion string `json:"gameVersion"` 24 | ProjectFileId int `json:"fileId"` 25 | ProjectFileName string `json:"filename"` 26 | } 27 | 28 | func json2Modinfo(jsonbyte []byte) ([]Modinfo, error) { 29 | d := apidata[[]Modinfo]{} 30 | err := json.Unmarshal(jsonbyte, &d) 31 | if err != nil { 32 | return nil, fmt.Errorf("json2Modinfo: %w", err) 33 | } 34 | return d.Data, nil 35 | } 36 | 37 | type Files struct { 38 | ID int `json:"id"` 39 | FileName string `json:"fileName"` 40 | FileDate string `json:"fileDate"` 41 | Dependencies []Dependencies `json:"dependencies"` 42 | DownloadUrl string `json:"downloadUrl"` 43 | GameVersion []string `json:"gameVersions"` 44 | ReleaseType int `json:"releaseType"` 45 | } 46 | 47 | type Dependencies struct { 48 | AddonId int `json:"modId"` 49 | Type int `json:"relationType"` 50 | } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xmdhs/cursemodownload 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/VictoriaMetrics/fastcache v1.12.0 7 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 8 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 9 | ) 10 | 11 | require ( 12 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 13 | github.com/golang/snappy v0.0.4 // indirect 14 | golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/VictoriaMetrics/fastcache v1.12.0 h1:vnVi/y9yKDcD9akmc4NqAoqgQhJrOwUF+j9LTgn4QDE= 2 | github.com/VictoriaMetrics/fastcache v1.12.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8= 3 | github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= 4 | github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= 5 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 6 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 10 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 11 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= 12 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 16 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 17 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 18 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 h1:D1v9ucDTYBtbz5vNuBbAhIMAGhQhJ6Ym5ah3maMVNX4= 20 | golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | _ "unsafe" 10 | 11 | "github.com/pkg/browser" 12 | _ "github.com/xmdhs/cursemodownload/curseapi" 13 | "github.com/xmdhs/cursemodownload/web" 14 | ) 15 | 16 | func main() { 17 | r := http.NewServeMux() 18 | r.HandleFunc("/curseforge", web.Index) 19 | r.HandleFunc("/curseforge/s", web.WebRoot) 20 | r.HandleFunc("/curseforge/info", web.Info) 21 | r.HandleFunc("/curseforge/download", web.Getdownloadlink) 22 | r.HandleFunc("/curseforge/history", web.History) 23 | s := http.Server{ 24 | Addr: "127.0.0.1:8082", 25 | ReadTimeout: 5 * time.Second, 26 | WriteTimeout: 20 * time.Second, 27 | Handler: r, 28 | } 29 | fmt.Println("WebServer Starting...") 30 | browser.OpenURL("http://127.0.0.1:8082/curseforge") 31 | log.Println(s.ListenAndServe()) 32 | } 33 | 34 | var apiaddr string 35 | 36 | //go:linkname api github.com/xmdhs/cursemodownload/curseapi.api 37 | var api string 38 | 39 | func init() { 40 | flag.StringVar(&apiaddr, "apiaddr", "https://addons-ecs.forgesvc.net", "api address") 41 | flag.Parse() 42 | 43 | api = apiaddr 44 | } 45 | -------------------------------------------------------------------------------- /web/genhtml.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | type historyS struct { 11 | Name string 12 | Version string 13 | WebsiteURL string 14 | VersionsListLink string 15 | Tr []string 16 | List []resultslist 17 | headS 18 | } 19 | 20 | type headS struct { 21 | Description string 22 | Title string 23 | } 24 | 25 | type pageS struct { 26 | headS 27 | Name string 28 | List []resultslist 29 | WebsiteURL string 30 | Link string 31 | } 32 | 33 | type resultslist struct { 34 | Title string 35 | Link string 36 | Txt template.HTML 37 | TdList []template.HTML 38 | } 39 | 40 | func (h *historyS) parse(w http.ResponseWriter) { 41 | h.Title += " - CurseForge mod" 42 | err := t.ExecuteTemplate(w, "history", h) 43 | if err != nil { 44 | log.Println(err) 45 | } 46 | } 47 | 48 | func (p *pageS) parse(w http.ResponseWriter, nextlink string) { 49 | if len(p.List) == 20 || nextlink != "" { 50 | p.Link = nextlink 51 | } 52 | p.Title += " - CurseForge mod" 53 | err := t.ExecuteTemplate(w, "page", p) 54 | if err != nil { 55 | log.Println(err) 56 | } 57 | } 58 | 59 | var t *template.Template 60 | 61 | func init() { 62 | var err error 63 | t, err = template.ParseFS(htmlfs, "html/*") 64 | if err != nil { 65 | panic(err) 66 | } 67 | w := &bytes.Buffer{} 68 | type Title struct { 69 | Title string 70 | Description string 71 | } 72 | err = t.ExecuteTemplate(w, "index", Title{Title: "CurseForge 搜索 - 搜索 CurseForge 上的东西并下载", Description: "搜索 CurseForge 上的东西并下载。"}) 73 | if err != nil { 74 | panic(err) 75 | } 76 | index = w.Bytes() 77 | } 78 | -------------------------------------------------------------------------------- /web/html.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import "embed" 4 | 5 | //go:embed html 6 | var htmlfs embed.FS 7 | -------------------------------------------------------------------------------- /web/html/footer.html: -------------------------------------------------------------------------------- 1 | {{define "footer"}} 2 | 5 | {{end}} -------------------------------------------------------------------------------- /web/html/head.html: -------------------------------------------------------------------------------- 1 | {{define "head"}} 2 | 3 | 4 | 5 | 6 | 7 | {{if .Description}} 8 | {{end}} 9 | {{.Title}} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 45 | 50 | 57 | 58 | {{end}} -------------------------------------------------------------------------------- /web/html/history.html: -------------------------------------------------------------------------------- 1 | {{define "history"}} 2 | 3 | 4 | {{template "head" .}} 5 | 6 | 7 |
8 |

CurseForge 搜索

10 |

{{.Name}}

11 |
12 | 13 |
14 | Home 15 |
16 |
17 | 18 | 19 |
20 | Versions List 21 |
22 |
23 | 24 |
25 | {{.Version}} 26 |
27 |
28 |
29 | 30 | 31 | 32 | {{range .Tr}}{{end}} 33 | 34 | {{range .List}} 35 | {{range .TdList}}{{end}} 36 | {{end}} 37 | 38 |
{{.}}
{{.}}
39 |
40 |
41 | 42 | 70 | {{template "footer" .}} 71 | 72 | 73 | 74 | 75 | {{end}} -------------------------------------------------------------------------------- /web/html/index.html: -------------------------------------------------------------------------------- 1 | {{define "index"}} 2 | 3 | 4 | {{template "head" .}} 5 | 6 | 7 |
8 | 11 |

CurseForge 搜索

13 | {{template "search" .}} 14 |
15 |
16 | {{template "footer" .}} 17 | 18 | 19 | 20 | {{end}} -------------------------------------------------------------------------------- /web/html/page.html: -------------------------------------------------------------------------------- 1 | {{define "page"}} 2 | 3 | 4 | {{template "head" .}} 5 | 6 | 7 |
8 |

CurseForge 搜索

10 | {{if .WebsiteURL}} 11 |

{{ .Name }}

12 | {{else}} 13 |

14 | {{ .Name }} 15 | 的搜索结果 16 |

17 | {{end}} 18 |
19 | {{range .List}} 20 | 21 |
22 |
23 |
24 |

{{ .Title}}

25 |
26 |
27 |

{{ .Txt}}

28 |
29 |
30 |
31 |
32 | {{end}} 33 |
34 | 35 | {{if .Link}} 36 |
37 | 38 | 查看更多 39 | 40 | {{else}} 41 | {{end}} 42 | {{template "footer" .}} 43 | 44 | 45 | {{end}} -------------------------------------------------------------------------------- /web/html/search.html: -------------------------------------------------------------------------------- 1 | {{define "search"}} 2 |
3 | 20 |
21 | 45 | {{end}} -------------------------------------------------------------------------------- /web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "html/template" 7 | "net/http" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/xmdhs/cursemodownload/curseapi" 14 | ) 15 | 16 | var sectionIds = map[string]int{ 17 | "1": 6, 18 | "2": 12, 19 | "3": 4471, 20 | "4": 17, 21 | } 22 | 23 | func WebRoot(w http.ResponseWriter, req *http.Request) { 24 | query := req.FormValue("q") 25 | page := req.FormValue("page") 26 | if page == "" { 27 | page = "0" 28 | } 29 | atype := req.FormValue("type") 30 | sectionId, ok := sectionIds[atype] 31 | if !ok { 32 | sectionId = 6 33 | } 34 | if len(query) > 100 { 35 | err := errors.New("关键词过长") 36 | http.Error(w, err.Error(), 500) 37 | return 38 | } 39 | query = strings.TrimSpace(query) 40 | _, err := strconv.Atoi(query) 41 | if err == nil { 42 | http.Redirect(w, req, "./info?id="+query, http.StatusMovedPermanently) 43 | return 44 | } 45 | 46 | i, err := strconv.ParseInt(page, 10, 64) 47 | if err != nil { 48 | http.Error(w, err.Error(), 500) 49 | return 50 | } 51 | page = strconv.FormatInt(i*20, 10) 52 | r, err := search(query, page, sectionId) 53 | if err != nil { 54 | http.Error(w, err.Error(), 500) 55 | return 56 | } 57 | if len(r) == 0 { 58 | http.NotFound(w, req) 59 | return 60 | } 61 | i++ 62 | page = strconv.FormatInt(i, 10) 63 | link := "./s?q=" + query + "&type=" + atype + "&page=" + page 64 | 65 | p := pageS{ 66 | headS: headS{ 67 | Description: "", 68 | Title: query, 69 | }, 70 | Name: query, 71 | List: r, 72 | Link: "", 73 | } 74 | p.parse(w, link) 75 | } 76 | 77 | var index []byte 78 | 79 | func Index(w http.ResponseWriter, req *http.Request) { 80 | w.Write(index) 81 | } 82 | 83 | func search(txt, offset string, sectionId int) ([]resultslist, error) { 84 | c, err := curseapi.Searchmod(txt, offset, sectionId) 85 | if err != nil { 86 | return nil, err 87 | } 88 | r := make([]resultslist, 0, len(c)) 89 | for _, v := range c { 90 | temp := resultslist{ 91 | Title: v.Name, 92 | Link: fmt.Sprintf("./info?id=%v", v.ID), 93 | Txt: template.HTML(template.HTMLEscapeString(v.Summary)), 94 | } 95 | r = append(r, temp) 96 | } 97 | return r, nil 98 | } 99 | 100 | func Info(w http.ResponseWriter, req *http.Request) { 101 | id := req.FormValue("id") 102 | c, err := curseapi.AddonInfo(id) 103 | if err != nil { 104 | http.Error(w, err.Error(), 500) 105 | return 106 | } 107 | var r []resultslist 108 | var title string 109 | set := make(map[string]struct{}) 110 | sort.Slice(c.GameVersionLatestFiles, func(i, j int) bool { 111 | v1 := c.GameVersionLatestFiles[i].GameVersion 112 | v2 := c.GameVersionLatestFiles[j].GameVersion 113 | v1l := strings.Split(v1, ".") 114 | v2l := strings.Split(v2, ".") 115 | a := 0 116 | if len(v1l) < len(v2l) { 117 | a = len(v1l) 118 | } else { 119 | a = len(v2l) 120 | } 121 | for i := 0; i < a; i++ { 122 | vn1, err := strconv.Atoi(v1l[i]) 123 | if err != nil { 124 | vn1 = 0 125 | } 126 | vn2, err := strconv.Atoi(v2l[i]) 127 | if err != nil { 128 | vn2 = 0 129 | } 130 | if vn1 > vn2 { 131 | return true 132 | } else if vn1 < vn2 { 133 | return false 134 | } 135 | } 136 | return len(v1l) > len(v2l) 137 | }) 138 | 139 | vers := strings.Builder{} 140 | 141 | if strconv.Itoa(c.ID) == id { 142 | title = c.Name 143 | r = make([]resultslist, 0, len(c.GameVersionLatestFiles)) 144 | for _, v := range c.GameVersionLatestFiles { 145 | if _, ok := set[v.GameVersion]; !ok { 146 | set[v.GameVersion] = struct{}{} 147 | link := `./history?id=` + id + "&ver=" + v.GameVersion 148 | temp := resultslist{ 149 | Title: template.HTMLEscapeString(v.GameVersion), 150 | Link: link, 151 | } 152 | 153 | vers.WriteString(v.GameVersion + " ") 154 | 155 | r = append(r, temp) 156 | } 157 | } 158 | } 159 | 160 | p := pageS{ 161 | headS: headS{ 162 | Description: c.Name + " - " + c.Summary + " - files download " + vers.String(), 163 | Title: title, 164 | }, 165 | Name: c.Name, 166 | List: r, 167 | Link: "", 168 | WebsiteURL: c.Links.WebsiteUrl, 169 | } 170 | p.parse(w, "") 171 | } 172 | 173 | func Getdownloadlink(w http.ResponseWriter, req *http.Request) { 174 | q := req.URL.Query() 175 | id := q.Get("id") 176 | if id == "" { 177 | http.Error(w, "", 500) 178 | return 179 | } 180 | link, err := curseapi.FileId2downloadlink(id) 181 | if err != nil { 182 | http.Error(w, err.Error(), 500) 183 | return 184 | } 185 | http.Redirect(w, req, link, http.StatusFound) 186 | } 187 | 188 | func History(w http.ResponseWriter, req *http.Request) { 189 | q := req.URL.Query() 190 | if len(q["id"]) == 0 || len(q["ver"]) == 0 { 191 | http.Error(w, "", 500) 192 | return 193 | } 194 | id, ver := q["id"][0], q["ver"][0] 195 | ch := make(chan curseapi.Modinfo, 10) 196 | errCh := make(chan error, 10) 197 | go func() { 198 | info, err := curseapi.AddonInfo(id) 199 | if err != nil { 200 | errCh <- err 201 | } 202 | ch <- info 203 | }() 204 | h, err := curseapi.Addonfiles(id, ver) 205 | if err != nil { 206 | http.Error(w, err.Error(), 500) 207 | return 208 | } 209 | var info curseapi.Modinfo 210 | select { 211 | case info = <-ch: 212 | case err = <-errCh: 213 | http.Error(w, err.Error(), 500) 214 | return 215 | } 216 | files := make([]curseapi.Files, 0) 217 | for _, v := range h { 218 | for _, vv := range v.GameVersion { 219 | if vv == ver { 220 | files = append(files, v) 221 | } 222 | } 223 | } 224 | r := make([]resultslist, 0) 225 | for _, v := range files { 226 | tdlist := make([]template.HTML, 0) 227 | if v.DownloadUrl == "" { 228 | tdlist = append(tdlist, template.HTML(template.HTMLEscapeString(v.FileName+" (can not get download link)"))) 229 | } else { 230 | tdlist = append(tdlist, template.HTML(``+template.HTMLEscapeString(v.FileName)+``)) 231 | } 232 | tdlist = append(tdlist, template.HTML(template.HTMLEscapeString(releaseType[v.ReleaseType]))) 233 | atime, err := time.Parse(time.RFC3339, v.FileDate) 234 | if err != nil { 235 | http.Error(w, err.Error(), 500) 236 | return 237 | } 238 | tdlist = append(tdlist, template.HTML(strconv.FormatInt(atime.Unix(), 10))) 239 | d := dependenciespase(v.Dependencies) 240 | if d == "" { 241 | d = "none" 242 | } 243 | tdlist = append(tdlist, template.HTML(d)) 244 | r = append(r, resultslist{ 245 | TdList: tdlist, 246 | }) 247 | } 248 | hs := historyS{ 249 | Name: info.Name, 250 | Version: ver, 251 | WebsiteURL: info.Links.WebsiteUrl, 252 | VersionsListLink: "/curseforge/info?id=" + id, 253 | Tr: tdname, 254 | List: r, 255 | headS: headS{ 256 | Description: info.Name + " - " + ver + " - " + info.Summary + " - files download", 257 | Title: info.Name + " - " + ver, 258 | }, 259 | } 260 | hs.parse(w) 261 | } 262 | 263 | var tdname = []string{"File Name", "Release Type", "File Date", "Dependencies"} 264 | 265 | var releaseType = map[int]string{ 266 | 1: "Release", 267 | 2: "Beta", 268 | 3: "Alpha", 269 | } 270 | 271 | func dependenciespase(dependencies []curseapi.Dependencies) string { 272 | s := strings.Builder{} 273 | i := 0 274 | for _, v := range dependencies { 275 | if v.Type == 3 { 276 | s.WriteString(`` + strconv.Itoa(v.AddonId) + ` `) 277 | i++ 278 | } 279 | } 280 | if i == 0 { 281 | return "" 282 | } 283 | return s.String() 284 | } 285 | 286 | func dependencies2url(dependencies curseapi.Dependencies) string { 287 | return "./info?id=" + strconv.Itoa(dependencies.AddonId) 288 | } 289 | --------------------------------------------------------------------------------