- google.go: use inline (?s) flag instead of regexp.DotAll second arg - youtube.go: remove Metadata field (not in MainResult contract) - config_test.go: fix expected engine count from 9 to 11 (google+youtube)
178 lines
4.4 KiB
Go
178 lines
4.4 KiB
Go
package engines
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/metamorphosis-dev/kafka/internal/contracts"
|
|
)
|
|
|
|
type YouTubeEngine struct {
|
|
client *http.Client
|
|
apiKey string
|
|
baseURL string
|
|
}
|
|
|
|
func (e *YouTubeEngine) Name() string { return "youtube" }
|
|
|
|
func (e *YouTubeEngine) Search(ctx context.Context, req contracts.SearchRequest) (contracts.SearchResponse, error) {
|
|
if strings.TrimSpace(req.Query) == "" {
|
|
return contracts.SearchResponse{Query: req.Query}, nil
|
|
}
|
|
|
|
if e.apiKey == "" {
|
|
e.apiKey = os.Getenv("YOUTUBE_API_KEY")
|
|
}
|
|
|
|
maxResults := 10
|
|
if req.Pageno > 1 {
|
|
maxResults = 20
|
|
}
|
|
|
|
u := e.baseURL + "/youtube/v3/search?" + url.Values{
|
|
"part": {"snippet"},
|
|
"q": {req.Query},
|
|
"type": {"video"},
|
|
"maxResults": {fmt.Sprintf("%d", maxResults)},
|
|
"key": {e.apiKey},
|
|
}.Encode()
|
|
|
|
if req.Language != "" && req.Language != "auto" {
|
|
lang := strings.Split(strings.ToLower(req.Language), "-")[0]
|
|
u += "&relevanceLanguage=" + lang
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, u, 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 != http.StatusOK {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
return contracts.SearchResponse{}, fmt.Errorf("youtube api error: status=%d body=%q", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var apiResp youtubeSearchResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
|
return contracts.SearchResponse{}, err
|
|
}
|
|
|
|
if apiResp.Error != nil {
|
|
return contracts.SearchResponse{}, fmt.Errorf("youtube api error: %s", apiResp.Error.Message)
|
|
}
|
|
|
|
results := make([]contracts.MainResult, 0, len(apiResp.Items))
|
|
for _, item := range apiResp.Items {
|
|
if item.ID.VideoID == "" {
|
|
continue
|
|
}
|
|
|
|
videoURL := "https://www.youtube.com/watch?v=" + item.ID.VideoID
|
|
urlPtr := videoURL
|
|
|
|
published := ""
|
|
if item.Snippet.PublishedAt != "" {
|
|
if t, err := time.Parse(time.RFC3339, item.Snippet.PublishedAt); err == nil {
|
|
published = t.Format("Jan 2, 2006")
|
|
}
|
|
}
|
|
|
|
content := item.Snippet.Description
|
|
if len(content) > 300 {
|
|
content = content[:300] + "..."
|
|
}
|
|
if published != "" {
|
|
content = "Published " + published + " · " + content
|
|
}
|
|
|
|
thumbnail := ""
|
|
if item.Snippet.Thumbnails.High.URL != "" {
|
|
thumbnail = item.Snippet.Thumbnails.High.URL
|
|
} else if item.Snippet.Thumbnails.Medium.URL != "" {
|
|
thumbnail = item.Snippet.Thumbnails.Medium.URL
|
|
}
|
|
|
|
results = append(results, contracts.MainResult{
|
|
Template: "videos.html",
|
|
Title: item.Snippet.Title,
|
|
URL: &urlPtr,
|
|
Content: content,
|
|
Thumbnail: thumbnail,
|
|
Engine: "youtube",
|
|
Score: 1.0,
|
|
Category: "videos",
|
|
Engines: []string{"youtube"},
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// YouTube API response types.
|
|
|
|
type youtubeSearchResponse struct {
|
|
Items []youtubeSearchItem `json:"items"`
|
|
PageInfo struct {
|
|
TotalResults int `json:"totalResults"`
|
|
ResultsPerPage int `json:"resultsPerPage"`
|
|
} `json:"pageInfo"`
|
|
NextPageToken string `json:"nextPageToken"`
|
|
Error *struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Errors []struct {
|
|
Domain string `json:"domain"`
|
|
Reason string `json:"reason"`
|
|
Message string `json:"message"`
|
|
} `json:"errors"`
|
|
} `json:"error"`
|
|
}
|
|
|
|
type youtubeSearchItem struct {
|
|
ID struct {
|
|
VideoID string `json:"videoId"`
|
|
} `json:"id"`
|
|
Snippet struct {
|
|
PublishedAt string `json:"publishedAt"`
|
|
ChannelID string `json:"channelId"`
|
|
ChannelTitle string `json:"channelTitle"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Thumbnails struct {
|
|
Default struct {
|
|
URL string `json:"url"`
|
|
} `json:"default"`
|
|
Medium struct {
|
|
URL string `json:"url"`
|
|
} `json:"medium"`
|
|
High struct {
|
|
URL string `json:"url"`
|
|
} `json:"high"`
|
|
} `json:"thumbnails"`
|
|
ResourceID struct {
|
|
VideoID string `json:"videoId"`
|
|
} `json:"resourceId"`
|
|
} `json:"snippet"`
|
|
}
|