feat: enrich RSS items without IDs via TMDB title search
Some checks failed
Build / build (push) Failing after 13m30s
Some checks failed
Build / build (push) Failing after 13m30s
Items from RSS feeds that have no media ID (tmdb/imdb/tvdb) are now enriched automatically using TMDB's search API. The release name is parsed to extract a clean title and year, then searched against TMDB to retrieve the TMDB ID before validation. - Add media/tmdb package with SearchMovies, SearchShows, and ExtractTitleAndYear (parses torrent release names) - Add EnrichFeedItemWithTmdbId to media.Client, called in rss/process.go before the ID validation switch - Add --run-now flag to nabarr run to trigger all feeds immediately - Wire media.Client through rss.Client and rssJob - Merge feature/add-tag-option (tag support for Sonarr/Radarr) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
7
media/tmdb/config.go
Normal file
7
media/tmdb/config.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package tmdb
|
||||
|
||||
type Config struct {
|
||||
ApiKey string `yaml:"api_key"`
|
||||
|
||||
Verbosity string `yaml:"verbosity,omitempty"`
|
||||
}
|
||||
87
media/tmdb/media.go
Normal file
87
media/tmdb/media.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/lucperkins/rek"
|
||||
|
||||
"github.com/l3uddz/nabarr/util"
|
||||
)
|
||||
|
||||
var ErrItemNotFound = errors.New("not found")
|
||||
|
||||
func (c *Client) SearchMovies(title string, year int) (int, error) {
|
||||
vals := url.Values{
|
||||
"api_key": []string{c.apiKey},
|
||||
"query": []string{title},
|
||||
}
|
||||
if year > 0 {
|
||||
vals.Set("year", strconv.Itoa(year))
|
||||
}
|
||||
|
||||
reqUrl, err := util.URLWithQuery(util.JoinURL(c.apiURL, "search", "movie"), vals)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("generate search movie url: %w", err)
|
||||
}
|
||||
|
||||
resp, err := rek.Get(reqUrl, rek.Client(c.http))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("request search movie: %w", err)
|
||||
}
|
||||
defer resp.Body().Close()
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
return 0, fmt.Errorf("validate search movie response: %s", resp.Status())
|
||||
}
|
||||
|
||||
b := new(movieSearchResponse)
|
||||
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
|
||||
return 0, fmt.Errorf("decode search movie response: %w", err)
|
||||
}
|
||||
|
||||
if len(b.Results) < 1 {
|
||||
return 0, ErrItemNotFound
|
||||
}
|
||||
|
||||
return b.Results[0].Id, nil
|
||||
}
|
||||
|
||||
func (c *Client) SearchShows(title string, year int) (int, error) {
|
||||
vals := url.Values{
|
||||
"api_key": []string{c.apiKey},
|
||||
"query": []string{title},
|
||||
}
|
||||
if year > 0 {
|
||||
vals.Set("first_air_date_year", strconv.Itoa(year))
|
||||
}
|
||||
|
||||
reqUrl, err := util.URLWithQuery(util.JoinURL(c.apiURL, "search", "tv"), vals)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("generate search tv url: %w", err)
|
||||
}
|
||||
|
||||
resp, err := rek.Get(reqUrl, rek.Client(c.http))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("request search tv: %w", err)
|
||||
}
|
||||
defer resp.Body().Close()
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
return 0, fmt.Errorf("validate search tv response: %s", resp.Status())
|
||||
}
|
||||
|
||||
b := new(tvSearchResponse)
|
||||
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
|
||||
return 0, fmt.Errorf("decode search tv response: %w", err)
|
||||
}
|
||||
|
||||
if len(b.Results) < 1 {
|
||||
return 0, ErrItemNotFound
|
||||
}
|
||||
|
||||
return b.Results[0].Id, nil
|
||||
}
|
||||
25
media/tmdb/parse.go
Normal file
25
media/tmdb/parse.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var yearRegex = regexp.MustCompile(`\b(19|20)\d{2}\b`)
|
||||
|
||||
// ExtractTitleAndYear parses a torrent release name and returns the clean title and year.
|
||||
// Example: "The.Housemaid.2025.FRENCH.1080p" → ("The Housemaid", 2025)
|
||||
func ExtractTitleAndYear(releaseName string) (string, int) {
|
||||
s := strings.ReplaceAll(releaseName, ".", " ")
|
||||
s = strings.ReplaceAll(s, "_", " ")
|
||||
|
||||
loc := yearRegex.FindStringIndex(s)
|
||||
if loc == nil {
|
||||
return strings.TrimSpace(s), 0
|
||||
}
|
||||
|
||||
year, _ := strconv.Atoi(s[loc[0]:loc[1]])
|
||||
title := strings.TrimSpace(s[:loc[0]])
|
||||
return title, year
|
||||
}
|
||||
13
media/tmdb/struct.go
Normal file
13
media/tmdb/struct.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package tmdb
|
||||
|
||||
type movieSearchResponse struct {
|
||||
Results []struct {
|
||||
Id int `json:"id"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
type tvSearchResponse struct {
|
||||
Results []struct {
|
||||
Id int `json:"id"`
|
||||
} `json:"results"`
|
||||
}
|
||||
33
media/tmdb/tmdb.go
Normal file
33
media/tmdb/tmdb.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/ratelimit"
|
||||
|
||||
"github.com/l3uddz/nabarr/logger"
|
||||
"github.com/l3uddz/nabarr/util"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
log zerolog.Logger
|
||||
http *http.Client
|
||||
|
||||
apiKey string
|
||||
apiURL string
|
||||
}
|
||||
|
||||
func New(cfg *Config) *Client {
|
||||
l := logger.Child(logger.WithLevel(cfg.Verbosity)).With().
|
||||
Str("media", "tmdb").Logger()
|
||||
|
||||
return &Client{
|
||||
log: l,
|
||||
http: util.NewRetryableHttpClient(30*time.Second, ratelimit.New(5, ratelimit.WithoutSlack), &l),
|
||||
|
||||
apiKey: cfg.ApiKey,
|
||||
apiURL: "https://api.themoviedb.org/3",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user