feat: add modpack versions API endpoint
This commit is contained in:
@@ -135,6 +135,7 @@ func main() {
|
|||||||
|
|
||||||
r.Route("/modpacks", func(r chi.Router) {
|
r.Route("/modpacks", func(r chi.Router) {
|
||||||
r.Post("/import", modpackHandler.ImportModpack)
|
r.Post("/import", modpackHandler.ImportModpack)
|
||||||
|
r.Get("/versions", modpackHandler.GetModpackVersions)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/users", func(r chi.Router) {
|
r.Route("/users", func(r chi.Router) {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"gitea.mrixs.me/minecraft-platform/backend/internal/core"
|
"gitea.mrixs.me/minecraft-platform/backend/internal/core"
|
||||||
"gitea.mrixs.me/minecraft-platform/backend/internal/core/importer"
|
"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)
|
msg, _ := json.Marshal(update)
|
||||||
h.Hub.BroadcastMessage(msg)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ func (i *CurseForgeImporter) getFileInfo(projectID, fileID int) (*CurseForgeFile
|
|||||||
return &fileInfo, nil
|
return &fileInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// findModpackBySlug ищет ID проекта по его "слагу" (части URL).
|
// FindModpackBySlug ищет ID проекта по его "слагу" (части URL).
|
||||||
func (i *CurseForgeImporter) findModpackBySlug(slug string) (int, error) {
|
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))
|
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)
|
req, err := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,30 +113,40 @@ func (i *CurseForgeImporter) findModpackBySlug(slug string) (int, error) {
|
|||||||
|
|
||||||
// getLatestModpackFileURL находит URL для скачивания последнего файла проекта.
|
// getLatestModpackFileURL находит URL для скачивания последнего файла проекта.
|
||||||
func (i *CurseForgeImporter) getLatestModpackFileURL(projectID int) (string, error) {
|
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)
|
apiURL := fmt.Sprintf("https://api.curseforge.com/v1/mods/%d/files", projectID)
|
||||||
req, err := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil)
|
req, err := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("x-api-key", i.APIKey)
|
req.Header.Set("x-api-key", i.APIKey)
|
||||||
|
|
||||||
resp, err := i.HTTPClient.Do(req)
|
resp, err := i.HTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var filesResp CurseForgeFilesResponse
|
var filesResp CurseForgeFilesResponse
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&filesResp); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&filesResp); err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filesResp.Data) == 0 {
|
return filesResp.Data, nil
|
||||||
return "", fmt.Errorf("no files found for projectID %d", projectID)
|
|
||||||
}
|
|
||||||
|
|
||||||
latestFile := filesResp.Data[0]
|
|
||||||
return latestFile.DownloadURL, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadModpackFromURL скачивает zip-архив модпака по URL страницы CurseForge.
|
// DownloadModpackFromURL скачивает zip-архив модпака по URL страницы CurseForge.
|
||||||
@@ -148,7 +158,7 @@ func (i *CurseForgeImporter) DownloadModpackFromURL(pageURL string) (string, err
|
|||||||
slug := path.Base(parsedURL.Path)
|
slug := path.Base(parsedURL.Path)
|
||||||
log.Printf("Importer: Extracted slug '%s' from URL", slug)
|
log.Printf("Importer: Extracted slug '%s' from URL", slug)
|
||||||
|
|
||||||
projectID, err := i.findModpackBySlug(slug)
|
projectID, err := i.FindModpackBySlug(slug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,16 +31,22 @@ type CurseForgeSearchResponse struct {
|
|||||||
|
|
||||||
// CurseForgeFilesResponse - ответ от эндпоинта получения файлов проекта
|
// CurseForgeFilesResponse - ответ от эндпоинта получения файлов проекта
|
||||||
type CurseForgeFilesResponse struct {
|
type CurseForgeFilesResponse struct {
|
||||||
Data []struct {
|
Data []CurseForgeFileData `json:"data"`
|
||||||
ID int `json:"id"`
|
|
||||||
FileName string `json:"fileName"`
|
|
||||||
DownloadURL string `json:"downloadUrl"`
|
|
||||||
} `json:"data"`
|
|
||||||
Pagination struct {
|
Pagination struct {
|
||||||
TotalCount int `json:"totalCount"`
|
TotalCount int `json:"totalCount"`
|
||||||
} `json:"pagination"`
|
} `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.
|
// CurseForgeFile представляет полную информацию о файле с API.
|
||||||
type CurseForgeFile struct {
|
type CurseForgeFile struct {
|
||||||
Data struct {
|
Data struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user