package engines import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/ashie/gosearch/internal/contracts" ) type CrossrefEngine struct { client *http.Client } func (e *CrossrefEngine) Name() string { return "crossref" } func (e *CrossrefEngine) Search(ctx context.Context, req contracts.SearchRequest) (contracts.SearchResponse, error) { if e == nil || e.client == nil { return contracts.SearchResponse{}, errors.New("crossref engine not initialized") } q := strings.TrimSpace(req.Query) if q == "" { return contracts.SearchResponse{Query: req.Query}, nil } offset := 20 * (req.Pageno - 1) args := url.Values{} args.Set("query", q) args.Set("offset", fmt.Sprintf("%d", offset)) endpoint := "https://api.crossref.org/works?" + args.Encode() httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return contracts.SearchResponse{}, err } resp, err := e.client.Do(httpReq) if err != nil { return contracts.SearchResponse{}, err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { body, _ := io.ReadAll(io.LimitReader(resp.Body, 16*1024)) return contracts.SearchResponse{}, fmt.Errorf("crossref upstream error: status=%d body=%q", resp.StatusCode, string(body)) } var api struct { Message struct { Items []crossrefItem `json:"items"` } `json:"message"` } if err := json.NewDecoder(resp.Body).Decode(&api); err != nil { return contracts.SearchResponse{}, err } results := make([]contracts.MainResult, 0, len(api.Message.Items)) for _, item := range api.Message.Items { title := "" if len(item.Title) > 0 { title = strings.TrimSpace(item.Title[0]) } content := strings.TrimSpace(item.Abstract) urlStr := strings.TrimSpace(item.URL) if urlStr == "" { urlStr = strings.TrimSpace(item.DOI) } pub := parseCrossrefDateParts(item.Published.DateParts) urlPtr := urlStr results = append(results, contracts.MainResult{ Template: "default.html", Title: title, Content: content, URL: &urlPtr, Pubdate: pub, Engine: "crossref", Score: 0, Category: "science", Priority: "", Positions: nil, Engines: []string{"crossref"}, }) } return contracts.SearchResponse{ Query: req.Query, NumberOfResults: len(results), Results: results, Answers: []map[string]any{}, Corrections: []string{}, Infoboxes: []map[string]any{}, Suggestions: []string{}, UnresponsiveEngines: [][2]string{}, }, nil } type crossrefItem struct { Type string `json:"type"` Title []string `json:"title"` URL string `json:"URL"` DOI string `json:"DOI"` Abstract string `json:"abstract"` Page string `json:"page"` Publisher string `json:"publisher"` Subject []string `json:"subject"` Published crossrefPublished `json:"published"` } type crossrefPublished struct { DateParts [][]int `json:"date-parts"` } func parseCrossrefDateParts(parts [][]int) *string { if len(parts) == 0 || len(parts[0]) == 0 { return nil } dp := parts[0] year := dp[0] month := 1 day := 1 if len(dp) >= 2 { month = dp[1] } if len(dp) >= 3 { day = dp[2] } t := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) formatted := t.Format("2006-01-02 00:00:00+0000") return &formatted }