From 1cdfe9fefcc996c18e03872b4ad0a366faebc94a Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Tue, 6 Jan 2026 19:09:15 +0300 Subject: [PATCH] feat: add modpack versions API endpoint --- cmd/server/main.go | 1 + internal/api/modpack_handler.go | 79 ++++++++++++++++++++++ internal/core/importer/curseforge.go | 34 ++++++---- internal/core/importer/curseforge_types.go | 16 +++-- 4 files changed, 113 insertions(+), 17 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index ce4c683..8585a7e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -135,6 +135,7 @@ func main() { r.Route("/modpacks", func(r chi.Router) { r.Post("/import", modpackHandler.ImportModpack) + r.Get("/versions", modpackHandler.GetModpackVersions) }) r.Route("/users", func(r chi.Router) { diff --git a/internal/api/modpack_handler.go b/internal/api/modpack_handler.go index ef7295b..5b2aef4 100644 --- a/internal/api/modpack_handler.go +++ b/internal/api/modpack_handler.go @@ -7,7 +7,9 @@ import ( "io" "log/slog" "net/http" + "net/url" "os" + "path" "gitea.mrixs.me/minecraft-platform/backend/internal/core" "gitea.mrixs.me/minecraft-platform/backend/internal/core/importer" @@ -203,3 +205,80 @@ func (h *ModpackHandler) updateJobStatus(ctx context.Context, jobID int, status msg, _ := json.Marshal(update) h.Hub.BroadcastMessage(msg) } + +// ModpackVersionResponse представляет версию модпака для ответа API +type ModpackVersionResponse struct { + FileID int `json:"file_id"` + DisplayName string `json:"display_name"` + FileName string `json:"file_name"` + FileDate string `json:"file_date"` + GameVersions []string `json:"game_versions"` +} + +// GetModpackVersions возвращает список доступных версий для модпака по URL +func (h *ModpackHandler) GetModpackVersions(w http.ResponseWriter, r *http.Request) { + pageURL := r.URL.Query().Get("url") + if pageURL == "" { + http.Error(w, "Missing 'url' query parameter", http.StatusBadRequest) + return + } + + apiKey := os.Getenv("CURSEFORGE_API_KEY") + if apiKey == "" { + http.Error(w, "CurseForge API key not configured", http.StatusInternalServerError) + return + } + + cfImporter := importer.NewCurseForgeImporter("", apiKey) + + // Извлекаем slug из URL + parsedURL, err := parseModpackURL(pageURL) + if err != nil { + http.Error(w, fmt.Sprintf("Invalid URL: %v", err), http.StatusBadRequest) + return + } + + // Ищем проект по slug + projectID, err := cfImporter.FindModpackBySlug(parsedURL) + if err != nil { + http.Error(w, fmt.Sprintf("Modpack not found: %v", err), http.StatusNotFound) + return + } + + // Получаем список файлов (версий) + files, err := cfImporter.GetModpackFiles(projectID) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to get versions: %v", err), http.StatusInternalServerError) + return + } + + // Преобразуем в ответ + var versions []ModpackVersionResponse + for _, f := range files { + versions = append(versions, ModpackVersionResponse{ + FileID: f.ID, + DisplayName: f.DisplayName, + FileName: f.FileName, + FileDate: f.FileDate, + GameVersions: f.GameVersions, + }) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(versions) +} + +// parseModpackURL извлекает slug из URL страницы CurseForge +func parseModpackURL(rawURL string) (string, error) { + parsed, err := url.Parse(rawURL) + if err != nil { + return "", err + } + // URL вида https://www.curseforge.com/minecraft/modpacks/all-the-mods-9 + // path.Base вернет "all-the-mods-9" + slug := path.Base(parsed.Path) + if slug == "" || slug == "." || slug == "/" { + return "", fmt.Errorf("could not extract modpack slug from URL") + } + return slug, nil +} diff --git a/internal/core/importer/curseforge.go b/internal/core/importer/curseforge.go index d6473f8..276eafb 100644 --- a/internal/core/importer/curseforge.go +++ b/internal/core/importer/curseforge.go @@ -84,8 +84,8 @@ func (i *CurseForgeImporter) getFileInfo(projectID, fileID int) (*CurseForgeFile return &fileInfo, nil } -// findModpackBySlug ищет ID проекта по его "слагу" (части URL). -func (i *CurseForgeImporter) findModpackBySlug(slug string) (int, error) { +// FindModpackBySlug ищет ID проекта по его "слагу" (части URL). +func (i *CurseForgeImporter) FindModpackBySlug(slug string) (int, error) { apiURL := fmt.Sprintf("https://api.curseforge.com/v1/mods/search?gameId=432&slug=%s", url.QueryEscape(slug)) req, err := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil) if err != nil { @@ -113,30 +113,40 @@ func (i *CurseForgeImporter) findModpackBySlug(slug string) (int, error) { // getLatestModpackFileURL находит URL для скачивания последнего файла проекта. func (i *CurseForgeImporter) getLatestModpackFileURL(projectID int) (string, error) { + files, err := i.GetModpackFiles(projectID) + if err != nil { + return "", err + } + + if len(files) == 0 { + return "", fmt.Errorf("no files found for projectID %d", projectID) + } + + latestFile := files[0] + return latestFile.DownloadURL, nil +} + +// GetModpackFiles возвращает список файлов (версий) для проекта. +func (i *CurseForgeImporter) GetModpackFiles(projectID int) ([]CurseForgeFileData, error) { apiURL := fmt.Sprintf("https://api.curseforge.com/v1/mods/%d/files", projectID) req, err := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil) if err != nil { - return "", err + return nil, err } req.Header.Set("x-api-key", i.APIKey) resp, err := i.HTTPClient.Do(req) if err != nil { - return "", err + return nil, err } defer resp.Body.Close() var filesResp CurseForgeFilesResponse if err := json.NewDecoder(resp.Body).Decode(&filesResp); err != nil { - return "", err + return nil, err } - if len(filesResp.Data) == 0 { - return "", fmt.Errorf("no files found for projectID %d", projectID) - } - - latestFile := filesResp.Data[0] - return latestFile.DownloadURL, nil + return filesResp.Data, nil } // DownloadModpackFromURL скачивает zip-архив модпака по URL страницы CurseForge. @@ -148,7 +158,7 @@ func (i *CurseForgeImporter) DownloadModpackFromURL(pageURL string) (string, err slug := path.Base(parsedURL.Path) log.Printf("Importer: Extracted slug '%s' from URL", slug) - projectID, err := i.findModpackBySlug(slug) + projectID, err := i.FindModpackBySlug(slug) if err != nil { return "", err } diff --git a/internal/core/importer/curseforge_types.go b/internal/core/importer/curseforge_types.go index 207ee16..a3c0d32 100644 --- a/internal/core/importer/curseforge_types.go +++ b/internal/core/importer/curseforge_types.go @@ -31,16 +31,22 @@ type CurseForgeSearchResponse struct { // CurseForgeFilesResponse - ответ от эндпоинта получения файлов проекта type CurseForgeFilesResponse struct { - Data []struct { - ID int `json:"id"` - FileName string `json:"fileName"` - DownloadURL string `json:"downloadUrl"` - } `json:"data"` + Data []CurseForgeFileData `json:"data"` Pagination struct { TotalCount int `json:"totalCount"` } `json:"pagination"` } +// CurseForgeFileData - данные о файле +type CurseForgeFileData struct { + ID int `json:"id"` + DisplayName string `json:"displayName"` + FileName string `json:"fileName"` + DownloadURL string `json:"downloadUrl"` + FileDate string `json:"fileDate"` + GameVersions []string `json:"gameVersions"` +} + // CurseForgeFile представляет полную информацию о файле с API. type CurseForgeFile struct { Data struct {