diff --git a/README.md b/README.md index d2318d2..364f31e 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ You can configure the server using the following environment variables: - `PORT`: (optional) The port that the server will listen to. Default: `7006` - `FLARESOLVERR_ADDRESS`: (optional) The address of the FlareSolverr instance. Default: `N/A` +- `MEILISEARCH_ADDRESS`: (optional) The address of the MeiliSearch instance. Default: `N/A` +- `MEILISEARCH_KEY`: (optional) The API key of the MeiliSearch instance. Default: `N/A` - `REDIS_HOST`: (optional) The address of the Redis instance. Default: `localhost` - `SHORT_LIVED_CACHE_EXPIRATION` (optional) The expiration time of the short-lived cache in duration format. Default: `30m` - This cache is used to cache homepage or search results. diff --git a/api/bludv.go b/api/bludv.go index ffa94f0..bcb56c0 100644 --- a/api/bludv.go +++ b/api/bludv.go @@ -40,10 +40,10 @@ func (i *Indexer) HandlerBluDVIndexer(w http.ResponseWriter, r *http.Request) { // URL encode query param q = url.QueryEscape(q) url := bludv.URL - if q != "" { - url = fmt.Sprintf("%s%s%s", url, bludv.SearchURL, q) - } else if page != "" { + if page != "" { url = fmt.Sprintf("%spage/%s", url, page) + } else { + url = fmt.Sprintf("%s%s%s", url, bludv.SearchURL, q) } fmt.Println("URL:>", url) @@ -78,9 +78,9 @@ func (i *Indexer) HandlerBluDVIndexer(w http.ResponseWriter, r *http.Request) { links = append(links, link) }) - var itChan = make(chan []IndexedTorrent) + var itChan = make(chan []schema.IndexedTorrent) var errChan = make(chan error) - indexedTorrents := []IndexedTorrent{} + indexedTorrents := []schema.IndexedTorrent{} for _, link := range links { go func(link string) { torrents, err := getTorrentsBluDV(ctx, i, link) @@ -110,16 +110,21 @@ func (i *Indexer) HandlerBluDVIndexer(w http.ResponseWriter, r *http.Request) { // remove the ones with zero similarity if len(indexedTorrents) > 20 && r.URL.Query().Get("filter_results") != "" && r.URL.Query().Get("q") != "" { - indexedTorrents = utils.Filter(indexedTorrents, func(it IndexedTorrent) bool { + indexedTorrents = utils.Filter(indexedTorrents, func(it schema.IndexedTorrent) bool { return it.Similarity > 0 }) } // sort by similarity - slices.SortFunc(indexedTorrents, func(i, j IndexedTorrent) int { + slices.SortFunc(indexedTorrents, func(i, j schema.IndexedTorrent) int { return int((j.Similarity - i.Similarity) * 1000) }) + // send to search index + go func() { + _ = i.search.IndexTorrents(indexedTorrents) + }() + w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(Response{ Results: indexedTorrents, @@ -130,8 +135,8 @@ func (i *Indexer) HandlerBluDVIndexer(w http.ResponseWriter, r *http.Request) { } } -func getTorrentsBluDV(ctx context.Context, i *Indexer, link string) ([]IndexedTorrent, error) { - var indexedTorrents []IndexedTorrent +func getTorrentsBluDV(ctx context.Context, i *Indexer, link string) ([]schema.IndexedTorrent, error) { + var indexedTorrents []schema.IndexedTorrent doc, err := getDocument(ctx, i, link) if err != nil { return nil, err @@ -191,7 +196,7 @@ func getTorrentsBluDV(ctx context.Context, i *Indexer, link string) ([]IndexedTo size = stableUniq(size) - var chanIndexedTorrent = make(chan IndexedTorrent) + var chanIndexedTorrent = make(chan schema.IndexedTorrent) // for each magnet link, create a new indexed torrent for it, magnetLink := range magnetLinks { @@ -231,7 +236,7 @@ func getTorrentsBluDV(ctx context.Context, i *Indexer, link string) ([]IndexedTo mySize = size[it] } - ixt := IndexedTorrent{ + ixt := schema.IndexedTorrent{ Title: appendAudioISO639_2Code(releaseTitle, magnetAudio), OriginalTitle: title, Details: link, diff --git a/api/comando_torrents.go b/api/comando_torrents.go index 1759a70..61746b3 100644 --- a/api/comando_torrents.go +++ b/api/comando_torrents.go @@ -93,9 +93,9 @@ func (i *Indexer) HandlerComandoIndexer(w http.ResponseWriter, r *http.Request) links = append(links, link) }) - var itChan = make(chan []IndexedTorrent) + var itChan = make(chan []schema.IndexedTorrent) var errChan = make(chan error) - indexedTorrents := []IndexedTorrent{} + indexedTorrents := []schema.IndexedTorrent{} for _, link := range links { go func(link string) { torrents, err := getTorrents(ctx, i, link) @@ -125,16 +125,21 @@ func (i *Indexer) HandlerComandoIndexer(w http.ResponseWriter, r *http.Request) // remove the ones with zero similarity if len(indexedTorrents) > 20 && r.URL.Query().Get("filter_results") != "" && r.URL.Query().Get("q") != "" { - indexedTorrents = utils.Filter(indexedTorrents, func(it IndexedTorrent) bool { + indexedTorrents = utils.Filter(indexedTorrents, func(it schema.IndexedTorrent) bool { return it.Similarity > 0 }) } // sort by similarity - slices.SortFunc(indexedTorrents, func(i, j IndexedTorrent) int { + slices.SortFunc(indexedTorrents, func(i, j schema.IndexedTorrent) int { return int((j.Similarity - i.Similarity) * 1000) }) + // send to search index + go func() { + _ = i.search.IndexTorrents(indexedTorrents) + }() + w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(Response{ Results: indexedTorrents, @@ -145,8 +150,8 @@ func (i *Indexer) HandlerComandoIndexer(w http.ResponseWriter, r *http.Request) } } -func getTorrents(ctx context.Context, i *Indexer, link string) ([]IndexedTorrent, error) { - var indexedTorrents []IndexedTorrent +func getTorrents(ctx context.Context, i *Indexer, link string) ([]schema.IndexedTorrent, error) { + var indexedTorrents []schema.IndexedTorrent doc, err := getDocument(ctx, i, link) if err != nil { return nil, err @@ -221,7 +226,7 @@ func getTorrents(ctx context.Context, i *Indexer, link string) ([]IndexedTorrent size = stableUniq(size) - var chanIndexedTorrent = make(chan IndexedTorrent) + var chanIndexedTorrent = make(chan schema.IndexedTorrent) // for each magnet link, create a new indexed torrent for it, magnetLink := range magnetLinks { @@ -261,7 +266,7 @@ func getTorrents(ctx context.Context, i *Indexer, link string) ([]IndexedTorrent mySize = size[it] } - ixt := IndexedTorrent{ + ixt := schema.IndexedTorrent{ Title: appendAudioISO639_2Code(releaseTitle, magnetAudio), OriginalTitle: title, Details: link, diff --git a/api/index.go b/api/index.go index 631ce8b..1dc629f 100644 --- a/api/index.go +++ b/api/index.go @@ -9,12 +9,14 @@ import ( "github.com/felipemarinho97/torrent-indexer/monitoring" "github.com/felipemarinho97/torrent-indexer/requester" "github.com/felipemarinho97/torrent-indexer/schema" + meilisearch "github.com/felipemarinho97/torrent-indexer/search" ) type Indexer struct { redis *cache.Redis metrics *monitoring.Metrics requester *requester.Requster + search *meilisearch.SearchIndexer } type IndexerMeta struct { @@ -23,32 +25,16 @@ type IndexerMeta struct { } type Response struct { - Results []IndexedTorrent `json:"results"` - Count int `json:"count"` + Results []schema.IndexedTorrent `json:"results"` + Count int `json:"count"` } -type IndexedTorrent struct { - Title string `json:"title"` - OriginalTitle string `json:"original_title"` - Details string `json:"details"` - Year string `json:"year"` - IMDB string `json:"imdb"` - Audio []schema.Audio `json:"audio"` - MagnetLink string `json:"magnet_link"` - Date time.Time `json:"date"` - InfoHash string `json:"info_hash"` - Trackers []string `json:"trackers"` - Size string `json:"size"` - LeechCount int `json:"leech_count"` - SeedCount int `json:"seed_count"` - Similarity float32 `json:"similarity"` -} - -func NewIndexers(redis *cache.Redis, metrics *monitoring.Metrics, req *requester.Requster) *Indexer { +func NewIndexers(redis *cache.Redis, metrics *monitoring.Metrics, req *requester.Requster, si *meilisearch.SearchIndexer) *Indexer { return &Indexer{ redis: redis, metrics: metrics, requester: req, + search: si, } } @@ -103,6 +89,15 @@ func HandlerIndex(w http.ResponseWriter, r *http.Request) { "description": "Get all manual torrents", }, }, + "/search": []map[string]interface{}{ + { + "method": "GET", + "description": "Search for cached torrents across all indexers", + "query_params": map[string]string{ + "q": "search query", + }, + }, + }, }, }) if err != nil { diff --git a/api/manual.go b/api/manual.go index 1054c95..9f25d06 100644 --- a/api/manual.go +++ b/api/manual.go @@ -26,7 +26,7 @@ type ManualIndexerRequest struct { func (i *Indexer) HandlerManualIndexer(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var req ManualIndexerRequest - indexedTorrents := []IndexedTorrent{} + indexedTorrents := []schema.IndexedTorrent{} // fetch from redis out, err := i.redis.Get(ctx, manualTorrentsRedisKey) @@ -97,7 +97,7 @@ func (i *Indexer) HandlerManualIndexer(w http.ResponseWriter, r *http.Request) { title := processTitle(releaseTitle, magnetAudio) - ixt := IndexedTorrent{ + ixt := schema.IndexedTorrent{ Title: appendAudioISO639_2Code(releaseTitle, magnetAudio), OriginalTitle: title, Audio: magnetAudio, diff --git a/api/search.go b/api/search.go new file mode 100644 index 0000000..06a69a0 --- /dev/null +++ b/api/search.go @@ -0,0 +1,55 @@ +package handler + +import ( + "encoding/json" + "net/http" + "strconv" + + meilisearch "github.com/felipemarinho97/torrent-indexer/search" +) + +// MeilisearchHandler handles HTTP requests for Meilisearch integration. +type MeilisearchHandler struct { + Module *meilisearch.SearchIndexer +} + +// NewMeilisearchHandler creates a new instance of MeilisearchHandler. +func NewMeilisearchHandler(module *meilisearch.SearchIndexer) *MeilisearchHandler { + return &MeilisearchHandler{Module: module} +} + +// SearchTorrentHandler handles the searching of torrent items. +func (h *MeilisearchHandler) SearchTorrentHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + query := r.URL.Query().Get("q") + if query == "" { + http.Error(w, "Query parameter 'q' is required", http.StatusBadRequest) + return + } + + limitStr := r.URL.Query().Get("limit") + limit := 10 // Default limit + if limitStr != "" { + var err error + limit, err = strconv.Atoi(limitStr) + if err != nil || limit <= 0 { + http.Error(w, "Invalid limit parameter", http.StatusBadRequest) + return + } + } + + results, err := h.Module.SearchTorrent(query, limit) + if err != nil { + http.Error(w, "Failed to search torrents", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(results); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} diff --git a/api/torrent_dos_filmes.go b/api/torrent_dos_filmes.go index c120f5b..82f11ef 100644 --- a/api/torrent_dos_filmes.go +++ b/api/torrent_dos_filmes.go @@ -77,9 +77,9 @@ func (i *Indexer) HandlerTorrentDosFilmesIndexer(w http.ResponseWriter, r *http. links = append(links, link) }) - var itChan = make(chan []IndexedTorrent) + var itChan = make(chan []schema.IndexedTorrent) var errChan = make(chan error) - indexedTorrents := []IndexedTorrent{} + indexedTorrents := []schema.IndexedTorrent{} for _, link := range links { go func(link string) { torrents, err := getTorrentsTorrentDosFilmes(ctx, i, link) @@ -109,16 +109,21 @@ func (i *Indexer) HandlerTorrentDosFilmesIndexer(w http.ResponseWriter, r *http. // remove the ones with zero similarity if len(indexedTorrents) > 20 && r.URL.Query().Get("filter_results") != "" && r.URL.Query().Get("q") != "" { - indexedTorrents = utils.Filter(indexedTorrents, func(it IndexedTorrent) bool { + indexedTorrents = utils.Filter(indexedTorrents, func(it schema.IndexedTorrent) bool { return it.Similarity > 0 }) } // sort by similarity - slices.SortFunc(indexedTorrents, func(i, j IndexedTorrent) int { + slices.SortFunc(indexedTorrents, func(i, j schema.IndexedTorrent) int { return int((j.Similarity - i.Similarity) * 1000) }) + // send to search index + go func() { + _ = i.search.IndexTorrents(indexedTorrents) + }() + w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(Response{ Results: indexedTorrents, @@ -129,8 +134,8 @@ func (i *Indexer) HandlerTorrentDosFilmesIndexer(w http.ResponseWriter, r *http. } } -func getTorrentsTorrentDosFilmes(ctx context.Context, i *Indexer, link string) ([]IndexedTorrent, error) { - var indexedTorrents []IndexedTorrent +func getTorrentsTorrentDosFilmes(ctx context.Context, i *Indexer, link string) ([]schema.IndexedTorrent, error) { + var indexedTorrents []schema.IndexedTorrent doc, err := getDocument(ctx, i, link) if err != nil { return nil, err @@ -190,7 +195,7 @@ func getTorrentsTorrentDosFilmes(ctx context.Context, i *Indexer, link string) ( size = stableUniq(size) - var chanIndexedTorrent = make(chan IndexedTorrent) + var chanIndexedTorrent = make(chan schema.IndexedTorrent) // for each magnet link, create a new indexed torrent for it, magnetLink := range magnetLinks { @@ -230,7 +235,7 @@ func getTorrentsTorrentDosFilmes(ctx context.Context, i *Indexer, link string) ( mySize = size[it] } - ixt := IndexedTorrent{ + ixt := schema.IndexedTorrent{ Title: appendAudioISO639_2Code(releaseTitle, magnetAudio), OriginalTitle: title, Details: link, diff --git a/docker-compose.yml b/docker-compose.yml index 04a90c0..a24d941 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,8 @@ services: - indexer environment: - REDIS_HOST=redis + - MEILISEARCH_ADDRESS=http://meilisearch:7700 + - MEILISEARCH_KEY=my-secret-key - FLARESOLVERR_ADDRESS=http://flaresolverr:8191 redis: @@ -20,5 +22,17 @@ services: networks: - indexer + # This container is not necessary for the indexer to work, + # deploy if you want to use the search feature + meilisearch: + image: getmeili/meilisearch:latest + container_name: meilisearch + restart: unless-stopped + networks: + - indexer + environment: + - MEILI_NO_ANALYTICS=true + - MEILI_MASTER_KEY=my-secret-key + networks: indexer: diff --git a/main.go b/main.go index 18467e6..75be6a7 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,9 @@ import ( handler "github.com/felipemarinho97/torrent-indexer/api" "github.com/felipemarinho97/torrent-indexer/cache" "github.com/felipemarinho97/torrent-indexer/monitoring" + "github.com/felipemarinho97/torrent-indexer/public" "github.com/felipemarinho97/torrent-indexer/requester" + meilisearch "github.com/felipemarinho97/torrent-indexer/search" "github.com/prometheus/client_golang/prometheus/promhttp" str2duration "github.com/xhit/go-str2duration/v2" @@ -16,6 +18,7 @@ import ( func main() { redis := cache.NewRedis() + searchIndex := meilisearch.NewSearchIndexer(os.Getenv("MEILISEARCH_ADDRESS"), os.Getenv("MEILISEARCH_KEY"), "torrents") metrics := monitoring.NewMetrics() metrics.Register() @@ -36,7 +39,8 @@ func main() { fmt.Println(err) } - indexers := handler.NewIndexers(redis, metrics, req) + indexers := handler.NewIndexers(redis, metrics, req, searchIndex) + search := handler.NewMeilisearchHandler(searchIndex) indexerMux := http.NewServeMux() metricsMux := http.NewServeMux() @@ -46,6 +50,8 @@ func main() { indexerMux.HandleFunc("/indexers/torrent-dos-filmes", indexers.HandlerTorrentDosFilmesIndexer) indexerMux.HandleFunc("/indexers/bludv", indexers.HandlerBluDVIndexer) indexerMux.HandleFunc("/indexers/manual", indexers.HandlerManualIndexer) + indexerMux.HandleFunc("/search", search.SearchTorrentHandler) + indexerMux.Handle("/ui/", http.StripPrefix("/ui/", http.FileServer(http.FS(public.UIFiles)))) metricsMux.Handle("/metrics", promhttp.Handler()) diff --git a/public/index.go b/public/index.go new file mode 100644 index 0000000..66d42f3 --- /dev/null +++ b/public/index.go @@ -0,0 +1,6 @@ +package public + +import "embed" + +//go:embed * +var UIFiles embed.FS diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..102347d --- /dev/null +++ b/public/index.html @@ -0,0 +1,127 @@ + + + + + + + Torrent Indexer + + + + + +
+ +
+

Torrent Indexer 🇧🇷

+

Find torrents with detailed information from torrent-indexer cache

+
+ + +
+ + +
+ + +
+ +
+
+ + + + + diff --git a/requester/flaresolverr.go b/requester/flaresolverr.go index a21bd2b..7f063ec 100644 --- a/requester/flaresolverr.go +++ b/requester/flaresolverr.go @@ -10,6 +10,8 @@ import ( "net/url" "strings" "sync" + + "github.com/felipemarinho97/torrent-indexer/utils" ) type FlareSolverr struct { @@ -243,8 +245,15 @@ func (f *FlareSolverr) Get(_url string) (io.ReadCloser, error) { return nil, fmt.Errorf("under attack") } + // check if the response is valid HTML + if !utils.IsValidHTML(response.Solution.Response) { + fmt.Printf("[FlareSolverr] Invalid HTML response from %s\n", _url) + response.Solution.Response = "" + } + // If the response body is empty but cookies are present, make a new request if response.Solution.Response == "" && len(response.Solution.Cookies) > 0 { + fmt.Printf("[FlareSolverr] Making a new request to %s with cookies\n", _url) // Create a new request with cookies client := &http.Client{} cookieJar, err := cookiejar.New(&cookiejar.Options{}) @@ -268,13 +277,22 @@ func (f *FlareSolverr) Get(_url string) (io.ReadCloser, error) { return nil, err } + // use the same user returned by the FlareSolverr + secondReq.Header.Set("User-Agent", response.Solution.UserAgent) + secondResp, err := client.Do(secondReq) if err != nil { return nil, err } + respByte := new(bytes.Buffer) + _, err = respByte.ReadFrom(secondResp.Body) + if err != nil { + return nil, err + } + // Return the body of the second request - return secondResp.Body, nil + return io.NopCloser(bytes.NewReader(respByte.Bytes())), nil } // Return the original response body diff --git a/schema/audio.go b/schema/audio.go index fa0d90c..8d1fe85 100644 --- a/schema/audio.go +++ b/schema/audio.go @@ -33,6 +33,8 @@ const ( AudioThai2 = "Tailandes" AudioTurkish = "Turco" AudioHindi = "Hindi" + AudioFarsi = "Persa" + AudioMalay = "Malaio" ) var AudioList = []Audio{ diff --git a/schema/indexed_torrent.go b/schema/indexed_torrent.go new file mode 100644 index 0000000..0c1ac82 --- /dev/null +++ b/schema/indexed_torrent.go @@ -0,0 +1,20 @@ +package schema + +import "time" + +type IndexedTorrent struct { + Title string `json:"title"` + OriginalTitle string `json:"original_title"` + Details string `json:"details"` + Year string `json:"year"` + IMDB string `json:"imdb"` + Audio []Audio `json:"audio"` + MagnetLink string `json:"magnet_link"` + Date time.Time `json:"date"` + InfoHash string `json:"info_hash"` + Trackers []string `json:"trackers"` + Size string `json:"size"` + LeechCount int `json:"leech_count"` + SeedCount int `json:"seed_count"` + Similarity float32 `json:"similarity"` +} diff --git a/search/meilisearch.go b/search/meilisearch.go new file mode 100644 index 0000000..9112e72 --- /dev/null +++ b/search/meilisearch.go @@ -0,0 +1,147 @@ +package meilisearch + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/felipemarinho97/torrent-indexer/schema" +) + +// SearchIndexer integrates with Meilisearch to index and search torrent items. +type SearchIndexer struct { + Client *http.Client + BaseURL string + APIKey string + IndexName string +} + +// NewSearchIndexer creates a new instance of SearchIndexer. +func NewSearchIndexer(baseURL, apiKey, indexName string) *SearchIndexer { + return &SearchIndexer{ + Client: &http.Client{Timeout: 10 * time.Second}, + BaseURL: baseURL, + APIKey: apiKey, + IndexName: indexName, + } +} + +// IndexTorrent indexes a single torrent item in Meilisearch. +func (t *SearchIndexer) IndexTorrent(torrent schema.IndexedTorrent) error { + url := fmt.Sprintf("%s/indexes/%s/documents", t.BaseURL, t.IndexName) + + torrentWithKey := struct { + Hash string `json:"id"` + schema.IndexedTorrent + }{ + Hash: torrent.InfoHash, + IndexedTorrent: torrent, + } + + jsonData, err := json.Marshal(torrentWithKey) + if err != nil { + return fmt.Errorf("failed to marshal torrent data: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + if t.APIKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.APIKey)) + } + + resp, err := t.Client.Do(req) + if err != nil { + return fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + return nil +} + +func (t *SearchIndexer) IndexTorrents(torrents []schema.IndexedTorrent) error { + url := fmt.Sprintf("%s/indexes/%s/documents", t.BaseURL, t.IndexName) + + torrentsWithKey := make([]struct { + Hash string `json:"id"` + schema.IndexedTorrent + }, 0, len(torrents)) + for _, torrent := range torrents { + torrentWithKey := struct { + Hash string `json:"id"` + schema.IndexedTorrent + }{ + Hash: torrent.InfoHash, + IndexedTorrent: torrent, + } + torrentsWithKey = append(torrentsWithKey, torrentWithKey) + } + + jsonData, err := json.Marshal(torrentsWithKey) + if err != nil { + return fmt.Errorf("failed to marshal torrent data: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + if t.APIKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.APIKey)) + } + + resp, err := t.Client.Do(req) + if err != nil { + return fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + return nil +} + +// SearchTorrent searches indexed torrents in Meilisearch based on the query. +func (t *SearchIndexer) SearchTorrent(query string, limit int) ([]schema.IndexedTorrent, error) { + url := fmt.Sprintf("%s/indexes/%s/search", t.BaseURL, t.IndexName) + requestBody := map[string]string{ + "q": query, + } + jsonData, err := json.Marshal(requestBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal search query: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + if t.APIKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.APIKey)) + } + + resp, err := t.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("search failed: %s", body) + } + + var result struct { + Hits []schema.IndexedTorrent `json:"hits"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to parse search response: %w", err) + } + + return result.Hits, nil +} diff --git a/utils/util.go b/utils/util.go index 66dbe63..fe1d462 100644 --- a/utils/util.go +++ b/utils/util.go @@ -1,5 +1,7 @@ package utils +import "regexp" + func Filter[A any](arr []A, f func(A) bool) []A { var res []A res = make([]A, 0) @@ -10,3 +12,21 @@ func Filter[A any](arr []A, f func(A) bool) []A { } return res } + +func IsValidHTML(input string) bool { + // Check for declaration (case-insensitive) + doctypeRegex := regexp.MustCompile(`(?i)`) + if !doctypeRegex.MatchString(input) { + return false + } + + // Check for and tags (case-insensitive) + htmlTagRegex := regexp.MustCompile(`(?i)[\s\S]*?`) + if !htmlTagRegex.MatchString(input) { + return false + } + + // Check for and tags (case-insensitive) + bodyTagRegex := regexp.MustCompile(`(?i)[\s\S]*?`) + return bodyTagRegex.MatchString(input) +}