package controllers import ( "fmt" "net/http" "strconv" "gitea.tbdevent.eu/TBD/reforger_crawler_main/initializers" "gitea.tbdevent.eu/TBD/reforger_crawler_main/models" "github.com/gin-gonic/gin" ) // convertToSimplifiedAddon converts a full Addon to SimplifiedAddon func convertToSimplifiedAddon(addon models.Addon) models.SimplifiedAddon { var simplifiedFiles []models.SimplifiedAddonFile for _, file := range addon.AddonFiles { simplifiedFiles = append(simplifiedFiles, models.SimplifiedAddonFile{ Path: file.Path, Hash: file.Hash, Version: file.Version, AddonID: file.AddonID, }) } return models.SimplifiedAddon{ ID: addon.ID, Name: addon.Name, Type: addon.Type, Summary: addon.Summary, SubscriberCount: addon.SubscriberCount, CurrentVersionNumber: addon.CurrentVersionNumber, Preview: addon.Preview, Author: addon.Author, AddonFiles: simplifiedFiles, } } // convertToSimplifiedAddonWithoutFiles converts a full Addon to SimplifiedAddon without files func convertToSimplifiedAddonWithoutFiles(addon models.Addon) models.SimplifiedAddon { return models.SimplifiedAddon{ ID: addon.ID, Name: addon.Name, Type: addon.Type, Summary: addon.Summary, SubscriberCount: addon.SubscriberCount, CurrentVersionNumber: addon.CurrentVersionNumber, Preview: addon.Preview, Author: addon.Author, AddonFiles: []models.SimplifiedAddonFile{}, // Empty slice } } func GetDuplicates(c *gin.Context) { id := c.Param("id") // First check if addon exists (lightweight query) var exists bool if err := initializers.DB.Model(&models.Addon{}).Select("1").Where("id = ?", id).Limit(1).Find(&exists).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Addon not found"}) return } // Get only the hashes from source addon files (don't load full addon yet) var sourceHashes []string if err := initializers.DB.Model(&models.AddonFile{}). Where("addon_id = ? AND hash != ''", id). Pluck("hash", &sourceHashes).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch source addon files"}) return } // Now load the full source addon for the response var sourceAddon models.Addon if err := initializers.DB.Preload("AddonFiles").First(&sourceAddon, "id = ?", id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Addon not found"}) return } simplifiedSourceAddon := convertToSimplifiedAddon(sourceAddon) if len(sourceHashes) == 0 { c.JSON(http.StatusOK, models.DuplicatesResponse{ SourceAddon: simplifiedSourceAddon, DuplicateAddons: make(map[string]models.AddonWithDuplicates), }) return } // Single query with JOIN to get all data type DuplicateRow struct { Hash string Path string Version string AddonID string AddonName string AddonType string Summary string Preview string Author string SubscriberCount int CurrentVersionNumber string } var duplicates []DuplicateRow err := initializers.DB.Table("addon_files af"). Select(`af.hash, af.path, af.version, af.addon_id, a.name as addon_name, a.type as addon_type, a.summary, a.preview, a.author, a.subscriber_count, a.current_version_number`). Joins("INNER JOIN addons a ON a.id = af.addon_id"). Joins("LEFT JOIN whitelisted_hashes wh ON wh.hash = af.hash"). Where(`af.hash IN ? AND af.addon_id != ? AND af.deleted_at IS NULL AND wh.hash IS NULL`, sourceHashes, id). Scan(&duplicates).Error if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch duplicates"}) return } // Build response from single result set response := models.DuplicatesResponse{ SourceAddon: simplifiedSourceAddon, DuplicateAddons: make(map[string]models.AddonWithDuplicates), } for _, dup := range duplicates { if _, exists := response.DuplicateAddons[dup.AddonID]; !exists { response.DuplicateAddons[dup.AddonID] = models.AddonWithDuplicates{ Addon: models.SimplifiedAddon{ ID: dup.AddonID, Name: dup.AddonName, Type: dup.AddonType, Summary: dup.Summary, Preview: dup.Preview, Author: dup.Author, SubscriberCount: dup.SubscriberCount, CurrentVersionNumber: dup.CurrentVersionNumber, AddonFiles: []models.SimplifiedAddonFile{}, }, Duplicates: []models.DuplicateFileInfo{}, } } entry := response.DuplicateAddons[dup.AddonID] entry.Duplicates = append(entry.Duplicates, models.DuplicateFileInfo{ Path: dup.Path, Hash: dup.Hash, Version: dup.Version, AddonID: dup.AddonID, }) response.DuplicateAddons[dup.AddonID] = entry } c.JSON(http.StatusOK, response) } /*func GetDuplicates(c *gin.Context) { id := c.Param("id") fmt.Println("Fetching addon with ID:", id) // Fetch the source addon with its files var sourceAddon models.Addon if err := initializers.DB.Preload("AddonFiles").First(&sourceAddon, "id = ?", id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Addon not found"}) return } // Convert to simplified addon simplifiedSourceAddon := convertToSimplifiedAddon(sourceAddon) // If the addon has no files, return early if len(sourceAddon.AddonFiles) == 0 { c.JSON(http.StatusOK, models.DuplicatesResponse{ SourceAddon: simplifiedSourceAddon, DuplicateAddons: make(map[string]models.AddonWithDuplicates), }) return } // Get all whitelisted hashes var whitelistedHashes []string if err := initializers.DB.Model(&models.WhitelistedHash{}).Pluck("hash", &whitelistedHashes).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch whitelisted hashes"}) return } // Create a map for faster lookup whitelistMap := make(map[string]bool) for _, hash := range whitelistedHashes { whitelistMap[hash] = true } // Extract all hashes from source addon files, excluding whitelisted ones var sourceHashes []string for _, file := range sourceAddon.AddonFiles { if file.Hash != "" && !whitelistMap[file.Hash] { sourceHashes = append(sourceHashes, file.Hash) } } if len(sourceHashes) == 0 { c.JSON(http.StatusOK, models.DuplicatesResponse{ SourceAddon: simplifiedSourceAddon, DuplicateAddons: make(map[string]models.AddonWithDuplicates), }) return } // Find all addon files that have matching hashes but belong to different addons var duplicateFiles []models.AddonFile if err := initializers.DB.Where("hash IN ? AND addon_id != ?", sourceHashes, id).Find(&duplicateFiles).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch duplicate files"}) return } // Group duplicate files by addon ID duplicatesByAddonID := make(map[string][]models.AddonFile) for _, file := range duplicateFiles { duplicatesByAddonID[file.AddonID] = append(duplicatesByAddonID[file.AddonID], file) } // Fetch addon information for each addon that has duplicates var addonIDs []string for addonID := range duplicatesByAddonID { addonIDs = append(addonIDs, addonID) } var duplicateAddons []models.Addon if len(addonIDs) > 0 { if err := initializers.DB.Where("id IN ?", addonIDs).Find(&duplicateAddons).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch duplicate addons"}) return } } // Build the response response := models.DuplicatesResponse{ SourceAddon: simplifiedSourceAddon, DuplicateAddons: make(map[string]models.AddonWithDuplicates), } // Create a map for easy addon lookup addonMap := make(map[string]models.Addon) for _, addon := range duplicateAddons { addonMap[addon.ID] = addon } // Build the duplicate addons response for addonID, files := range duplicatesByAddonID { var duplicateFileInfos []models.DuplicateFileInfo for _, file := range files { duplicateFileInfos = append(duplicateFileInfos, models.DuplicateFileInfo{ Path: file.Path, Hash: file.Hash, Version: file.Version, AddonID: file.AddonID, }) } if addon, exists := addonMap[addonID]; exists { response.DuplicateAddons[addonID] = models.AddonWithDuplicates{ Addon: convertToSimplifiedAddonWithoutFiles(addon), Duplicates: duplicateFileInfos, } } } c.JSON(http.StatusOK, response) }*/ func GetPossibleAddons(c *gin.Context) { query := c.Query("q") if query == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Query parameter 'q' is required"}) return } // Set default limit limit := 20 if limitParam := c.Query("limit"); limitParam != "" { if parsedLimit, err := strconv.Atoi(limitParam); err == nil && parsedLimit > 0 && parsedLimit <= 100 { limit = parsedLimit } } fmt.Printf("Searching for addons with query: %s (limit: %d)\n", query, limit) var addons []models.Addon // Search by ID (exact match) or by name (case-insensitive partial match) // Using OR condition to search both ID and name fields dbQuery := initializers.DB.Where("id = ? OR LOWER(name) LIKE LOWER(?)", query, "%"+query+"%"). Order("subscriber_count DESC"). // Order by popularity Limit(limit) if err := dbQuery.Find(&addons).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search addons"}) return } // Convert to search results var results []models.AddonSearchResult for _, addon := range addons { results = append(results, models.AddonSearchResult{ ID: addon.ID, Name: addon.Name, Type: addon.Type, Summary: addon.Summary, Preview: addon.Preview, SubscriberCount: addon.SubscriberCount, CurrentVersionNumber: addon.CurrentVersionNumber, Author: addon.Author, }) } response := models.SearchResponse{ Query: query, Results: results, Total: len(results), } c.JSON(http.StatusOK, response) }