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 @@ + + + +
+ + +Find torrents with detailed information from torrent-indexer cache
+